UpnpDatabasePlugin.cxx 17.5 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
#include "lib/upnp/Discovery.hxx"
#include "lib/upnp/ContentDirectoryService.hxx"
27
#include "db/Interface.hxx"
Max Kellermann's avatar
Max Kellermann committed
28 29 30 31 32
#include "db/DatabasePlugin.hxx"
#include "db/Selection.hxx"
#include "db/DatabaseError.hxx"
#include "db/LightDirectory.hxx"
#include "db/LightSong.hxx"
33
#include "db/Stats.hxx"
34
#include "config/Block.hxx"
35 36
#include "tag/Builder.hxx"
#include "tag/Table.hxx"
37
#include "tag/Mask.hxx"
38
#include "fs/Traits.hxx"
39 40
#include "Log.hxx"
#include "SongFilter.hxx"
41
#include "util/SplitString.hxx"
42 43 44 45 46 47 48 49 50

#include <string>
#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
		tag = &tag2;
65
		mtime = std::chrono::system_clock::time_point::min();
66
		start_time = end_time = SongTime::zero();
67 68 69
	}
};

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

public:
76 77 78
	explicit UpnpDatabase(EventLoop &_event_loop)
		:Database(upnp_db_plugin),
		 event_loop(_event_loop) {}
79

80 81 82
	static Database *Create(EventLoop &main_event_loop,
				EventLoop &io_event_loop,
				DatabaseListener &listener,
83
				const ConfigBlock &block);
84

85 86 87
	void Open() override;
	void Close() override;
	const LightSong *GetSong(const char *uri_utf8) const override;
88
	void ReturnSong(const LightSong *song) const override;
89

90 91 92 93
	void Visit(const DatabaseSelection &selection,
		   VisitDirectory visit_directory,
		   VisitSong visit_song,
		   VisitPlaylist visit_playlist) const override;
94

95
	void VisitUniqueTags(const DatabaseSelection &selection,
96
			     TagType tag_type, TagMask group_mask,
97 98 99
			     VisitTag visit_tag) const override;

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

Max Kellermann's avatar
Max Kellermann committed
101
	std::chrono::system_clock::time_point GetUpdateStamp() const noexcept override {
102
		return std::chrono::system_clock::time_point::min();
103
	}
104 105

private:
106
	void VisitServer(const ContentDirectoryService &server,
107
			 std::forward_list<std::string> &&vpath,
108 109 110
			 const DatabaseSelection &selection,
			 VisitDirectory visit_directory,
			 VisitSong visit_song,
111
			 VisitPlaylist visit_playlist) const;
112 113 114 115 116

	/**
	 * Run an UPnP search according to MPD parameters, and
	 * visit_song the results.
	 */
117
	void SearchSongs(const ContentDirectoryService &server,
118 119
			 const char *objid,
			 const DatabaseSelection &selection,
120
			 VisitSong visit_song) const;
121

122 123 124
	UPnPDirContent SearchSongs(const ContentDirectoryService &server,
				   const char *objid,
				   const DatabaseSelection &selection) const;
125

126
	UPnPDirObject Namei(const ContentDirectoryService &server,
127
			    std::forward_list<std::string> &&vpath) const;
128 129 130 131

	/**
	 * Take server and objid, return metadata.
	 */
132 133
	UPnPDirObject ReadNode(const ContentDirectoryService &server,
			       const char *objid) const;
134 135 136 137 138 139

	/**
	 * 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).
	 */
140 141
	std::string BuildPath(const ContentDirectoryService &server,
			      const UPnPDirObject& dirent) const;
142 143 144
};

Database *
145
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
146
		     gcc_unused DatabaseListener &listener,
147
		     const ConfigBlock &)
148
{
149
	return new UpnpDatabase(io_event_loop);
150 151
}

152 153
void
UpnpDatabase::Open()
154
{
155
	handle = UpnpClientGlobalInit();
156

157
	discovery = new UPnPDeviceDirectory(event_loop, handle);
158 159 160
	try {
		discovery->Start();
	} catch (...) {
161
		delete discovery;
162
		UpnpClientGlobalFinish();
163
		throw;
164 165 166 167 168 169
	}
}

void
UpnpDatabase::Close()
{
170
	delete discovery;
171
	UpnpClientGlobalFinish();
172 173 174
}

void
175
UpnpDatabase::ReturnSong(const LightSong *_song) const
176
{
177
	assert(_song != nullptr);
178

179 180
	UpnpSong *song = (UpnpSong *)const_cast<LightSong *>(_song);
	delete song;
181 182 183 184
}

// Get song info by path. We can receive either the id path, or the titles
// one
185
const LightSong *
186
UpnpDatabase::GetSong(const char *uri) const
187
{
188 189
	auto vpath = SplitString(uri, '/');
	if (vpath.empty())
190 191
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
192

193
	auto server = discovery->GetServer(vpath.front().c_str());
194 195
	vpath.pop_front();

196 197 198 199
	if (vpath.empty())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");

200
	UPnPDirObject dirent;
201
	if (vpath.front() != rootid) {
202
		dirent = Namei(server, std::move(vpath));
203
	} else {
204 205 206 207 208 209
		vpath.pop_front();
		if (vpath.empty())
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such song");

		dirent = ReadNode(server, vpath.front().c_str());
210 211
	}

212
	return new UpnpSong(std::move(dirent), uri);
213 214 215 216 217 218 219 220
}

/**
 * Double-quote a string, adding internal backslash escaping.
 */
static void
dquote(std::string &out, const char *in)
{
221
	out.push_back('"');
222 223 224 225 226

	for (; *in != 0; ++in) {
		switch(*in) {
		case '\\':
		case '"':
227
			out.push_back('\\');
228 229
			break;
		}
230 231

		out.push_back(*in);
232 233
	}

234
	out.push_back('"');
235 236 237 238
}

// Run an UPnP search, according to MPD parameters. Return results as
// UPnP items
239
UPnPDirContent
240
UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
241
			  const char *objid,
242
			  const DatabaseSelection &selection) const
243 244 245
{
	const SongFilter *filter = selection.filter;
	if (selection.filter == nullptr)
246
		return UPnPDirContent();
247

248
	const auto searchcaps = server.getSearchCapabilities(handle);
249
	if (searchcaps.empty())
250
		return UPnPDirContent();
251 252 253 254 255 256 257 258 259

	std::string cond;
	for (const auto &item : filter->GetItems()) {
		switch (auto tag = item.GetTag()) {
		case LOCATE_TAG_ANY_TYPE:
			{
				if (!cond.empty()) {
					cond += " and ";
				}
260
				cond += '(';
261 262 263 264 265 266 267 268 269 270 271 272
				bool first(true);
				for (const auto& cap : searchcaps) {
					if (first)
						first = false;
					else
						cond += " or ";
					cond += cap;
					if (item.GetFoldCase()) {
						cond += " contains ";
					} else {
						cond += " = ";
					}
273
					dquote(cond, item.GetValue());
274
				}
275
				cond += ')';
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
			}
			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 += " = ";
			}
309
			dquote(cond, item.GetValue());
310 311 312
		}
	}

313
	return server.search(handle, objid, cond.c_str());
314 315
}

316
static void
317
visitSong(const UPnPDirObject &meta, const char *path,
318
	  const DatabaseSelection &selection,
319
	  VisitSong visit_song)
320 321
{
	if (!visit_song)
322
		return;
323

324 325
	LightSong song;
	song.directory = nullptr;
326
	song.uri = path;
327 328
	song.real_uri = meta.url.c_str();
	song.tag = &meta.tag;
329
	song.mtime = std::chrono::system_clock::time_point::min();
330
	song.start_time = song.end_time = SongTime::zero();
331

332 333
	if (selection.Match(song))
		visit_song(song);
334 335 336 337 338 339 340
}

/**
 * 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.
 */
341
static std::string
342 343 344 345 346 347
songPath(const std::string &servername,
	 const std::string &objid)
{
	return servername + "/" + rootid + "/" + objid;
}

348
void
349
UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
350 351
			  const char *objid,
			  const DatabaseSelection &selection,
352
			  VisitSong visit_song) const
353 354
{
	if (!visit_song)
355
		return;
356

357
	for (auto &dirent : SearchSongs(server, objid, selection).objects) {
358 359 360 361
		if (dirent.type != UPnPDirObject::Type::ITEM ||
		    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
			continue;

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
		// 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.
378
		const std::string path = songPath(server.getFriendlyName(),
379
						  dirent.id);
380 381
		visitSong(std::move(dirent), path.c_str(),
			  selection, visit_song);
382 383 384
	}
}

385
UPnPDirObject
386
UpnpDatabase::ReadNode(const ContentDirectoryService &server,
387
		       const char *objid) const
388
{
389
	auto dirbuf = server.getMetadata(handle, objid);
390 391
	if (dirbuf.objects.size() != 1)
		throw std::runtime_error("Bad resource");
392

393
	return std::move(dirbuf.objects.front());
394 395
}

396
std::string
397
UpnpDatabase::BuildPath(const ContentDirectoryService &server,
398
			const UPnPDirObject& idirent) const
399
{
400
	const char *pid = idirent.id.c_str();
401
	std::string path;
402
	while (strcmp(pid, rootid) != 0) {
403
		auto dirent = ReadNode(server, pid);
404
		pid = dirent.parent_id.c_str();
405 406 407 408 409 410

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

413
	return PathTraitsUTF8::Build(server.getFriendlyName(),
414
				     path.c_str());
415 416 417
}

// Take server and internal title pathname and return objid and metadata.
418
UPnPDirObject
419
UpnpDatabase::Namei(const ContentDirectoryService &server,
420
		    std::forward_list<std::string> &&vpath) const
421
{
422
	if (vpath.empty())
423
		// looking for root info
424
		return ReadNode(server, rootid);
425

426 427
	std::string objid(rootid);

428
	// Walk the path elements, read each directory and try to find the next one
429
	while (true) {
430
		auto dirbuf = server.readDir(handle, objid.c_str());
431 432

		// Look for the name in the sub-container list
433
		UPnPDirObject *child = dirbuf.FindObject(vpath.front().c_str());
434 435 436
		if (child == nullptr)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such object");
437

438 439
		vpath.pop_front();
		if (vpath.empty())
440
			return std::move(*child);
441

442 443 444
		if (child->type != UPnPDirObject::Type::CONTAINER)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not a container");
445

446
		objid = std::move(child->id);
447 448 449
	}
}

450
static void
451 452
VisitItem(const UPnPDirObject &object, const char *uri,
	  const DatabaseSelection &selection,
453
	  VisitSong visit_song, VisitPlaylist visit_playlist)
454 455 456 457 458
{
	assert(object.type == UPnPDirObject::Type::ITEM);

	switch (object.item_class) {
	case UPnPDirObject::ItemClass::MUSIC:
459 460
		visitSong(object, uri, selection, visit_song);
		break;
461 462 463 464 465 466 467 468 469 470 471

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

472
		break;
473 474

	case UPnPDirObject::ItemClass::UNKNOWN:
475
		break;
476 477 478
	}
}

479
static void
480 481 482 483
VisitObject(const UPnPDirObject &object, const char *uri,
	    const DatabaseSelection &selection,
	    VisitDirectory visit_directory,
	    VisitSong visit_song,
484
	    VisitPlaylist visit_playlist)
485 486 487 488 489 490 491
{
	switch (object.type) {
	case UPnPDirObject::Type::UNKNOWN:
		assert(false);
		gcc_unreachable();

	case UPnPDirObject::Type::CONTAINER:
492
		if (visit_directory)
493 494
			visit_directory(LightDirectory(uri,
						       std::chrono::system_clock::time_point::min()));
495
		break;
496 497

	case UPnPDirObject::Type::ITEM:
498 499 500
		VisitItem(object, uri, selection,
			  visit_song, visit_playlist);
		break;
501 502 503
	}
}

504 505
// vpath is a parsed and writeable version of selection.uri. There is
// really just one path parameter.
506
void
507
UpnpDatabase::VisitServer(const ContentDirectoryService &server,
508
			  std::forward_list<std::string> &&vpath,
509 510 511
			  const DatabaseSelection &selection,
			  VisitDirectory visit_directory,
			  VisitSong visit_song,
512
			  VisitPlaylist visit_playlist) const
513 514 515 516 517 518 519 520 521
{
	/* 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 */
522
	if (!vpath.empty() && vpath.front() == rootid) {
523 524
		vpath.pop_front();
		if (vpath.empty())
525
			return;
526

527 528 529
		const std::string objid(std::move(vpath.front()));
		vpath.pop_front();
		if (!vpath.empty())
530 531
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not found");
532

533
		if (visit_song) {
534
			auto dirent = ReadNode(server, objid.c_str());
535

536
			if (dirent.type != UPnPDirObject::Type::ITEM ||
537 538 539
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
						    "Not found");
540

541
			std::string path = songPath(server.getFriendlyName(),
542
						    dirent.id);
543 544
			visitSong(std::move(dirent), path.c_str(),
				  selection, visit_song);
545
		}
546 547

		return;
548 549 550
	}

	// Translate the target path into an object id and the associated metadata.
551
	const auto tdirent = Namei(server, std::move(vpath));
552 553 554 555 556

	/* 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. */
557 558 559 560
	if (selection.recursive && selection.filter) {
		SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
		return;
	}
561

Max Kellermann's avatar
Max Kellermann committed
562 563 564 565
	const char *const base_uri = selection.uri.empty()
		? server.getFriendlyName()
		: selection.uri.c_str();

566
	if (tdirent.type == UPnPDirObject::Type::ITEM) {
567 568 569 570
		VisitItem(tdirent, base_uri,
			  selection,
			  visit_song, visit_playlist);
		return;
571 572 573 574 575
	}

	/* 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. */
576
	for (const auto &dirent : server.readDir(handle, tdirent.id.c_str()).objects) {
577 578
		const std::string uri = PathTraitsUTF8::Build(base_uri,
							      dirent.name.c_str());
579 580 581 582
		VisitObject(dirent, uri.c_str(),
			    selection,
			    visit_directory,
			    visit_song, visit_playlist);
583 584 585 586
	}
}

// Deal with the possibly multiple servers, call VisitServer if needed.
587
void
588 589 590
UpnpDatabase::Visit(const DatabaseSelection &selection,
		    VisitDirectory visit_directory,
		    VisitSong visit_song,
591
		    VisitPlaylist visit_playlist) const
592
{
593
	auto vpath = SplitString(selection.uri.c_str(), '/');
594
	if (vpath.empty()) {
595
		for (const auto &server : discovery->GetDirectories()) {
596
			if (visit_directory) {
597 598
				const LightDirectory d(server.getFriendlyName(),
						       std::chrono::system_clock::time_point::min());
599
				visit_directory(d);
600
			}
601

602
			if (selection.recursive)
603
				VisitServer(server, std::move(vpath), selection,
604 605
					    visit_directory, visit_song,
					    visit_playlist);
606
		}
607

608
		return;
609 610 611
	}

	// We do have a path: the first element selects the server
612
	std::string servername(std::move(vpath.front()));
613
	vpath.pop_front();
614

615
	auto server = discovery->GetServer(servername.c_str());
616
	VisitServer(server, std::move(vpath), selection,
617
		    visit_directory, visit_song, visit_playlist);
618 619
}

620
void
621
UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
622
			      TagType tag, gcc_unused TagMask group_mask,
623
			      VisitTag visit_tag) const
624
{
625 626 627
	// TODO: use group_mask

	if (!visit_tag)
628
		return;
629 630

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

634 635 636 637 638
		for (const auto &dirent : dirbuf.objects) {
			if (dirent.type != UPnPDirObject::Type::ITEM ||
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				continue;

639
			const char *value = dirent.tag.GetValue(tag);
640 641
			if (value != nullptr) {
				values.emplace(value);
642
			}
643 644 645
		}
	}

646 647 648
	for (const auto& value : values) {
		TagBuilder builder;
		builder.AddItem(tag, value.c_str());
649
		visit_tag(builder.Commit());
650
	}
651 652
}

653 654
DatabaseStats
UpnpDatabase::GetStats(const DatabaseSelection &) const
655 656 657
{
	/* Note: this gets called before the daemonizing so we can't
	   reallyopen this would be a problem if we had real stats */
658
	DatabaseStats stats;
659
	stats.Clear();
660
	return stats;
661 662 663 664
}

const DatabasePlugin upnp_db_plugin = {
	"upnp",
665
	0,
666 667
	UpnpDatabase::Create,
};