UpnpDatabasePlugin.cxx 17.8 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
#include "db/DatabasePlugin.hxx"
#include "db/Selection.hxx"
30
#include "db/VHelper.hxx"
Max Kellermann's avatar
Max Kellermann committed
31 32
#include "db/DatabaseError.hxx"
#include "db/LightDirectory.hxx"
33
#include "song/LightSong.hxx"
34 35
#include "song/Filter.hxx"
#include "song/TagSongFilter.hxx"
36
#include "db/Stats.hxx"
37
#include "config/Block.hxx"
38 39
#include "tag/Builder.hxx"
#include "tag/Table.hxx"
40
#include "tag/Mask.hxx"
41
#include "fs/Traits.hxx"
42
#include "Log.hxx"
43
#include "util/SplitString.hxx"
44 45 46 47 48 49 50 51 52

#include <string>
#include <set>

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

static const char *const rootid = "0";

53 54 55 56 57 58 59 60 61
class UpnpSongData {
protected:
	std::string uri;
	Tag tag;

	template<typename U, typename T>
	UpnpSongData(U &&_uri, T &&_tag) noexcept
		:uri(std::forward<U>(_uri)), tag(std::forward<T>(_tag)) {}
};
62

63 64
class UpnpSong : UpnpSongData, public LightSong {
	std::string real_uri2;
65 66

public:
67
	UpnpSong(UPnPDirObject &&object, std::string &&_uri) noexcept
68 69 70
		:UpnpSongData(std::move(_uri), std::move(object.tag)),
		 LightSong(UpnpSongData::uri.c_str(), UpnpSongData::tag),
		 real_uri2(std::move(object.url)) {
71
		real_uri = real_uri2.c_str();
72 73 74
	}
};

75
class UpnpDatabase : public Database {
76
	EventLoop &event_loop;
77
	UpnpClient_Handle handle;
78
	UPnPDeviceDirectory *discovery;
79 80

public:
81
	explicit UpnpDatabase(EventLoop &_event_loop) noexcept
82 83
		:Database(upnp_db_plugin),
		 event_loop(_event_loop) {}
84

85 86 87
	static Database *Create(EventLoop &main_event_loop,
				EventLoop &io_event_loop,
				DatabaseListener &listener,
88
				const ConfigBlock &block) noexcept;
89

90
	void Open() override;
91
	void Close() noexcept override;
92
	const LightSong *GetSong(const char *uri_utf8) const override;
93
	void ReturnSong(const LightSong *song) const noexcept override;
94

95 96 97 98
	void Visit(const DatabaseSelection &selection,
		   VisitDirectory visit_directory,
		   VisitSong visit_song,
		   VisitPlaylist visit_playlist) const override;
99

100
	void VisitUniqueTags(const DatabaseSelection &selection,
101
			     TagType tag_type, TagMask group_mask,
102 103 104
			     VisitTag visit_tag) const override;

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

Max Kellermann's avatar
Max Kellermann committed
106
	std::chrono::system_clock::time_point GetUpdateStamp() const noexcept override {
107
		return std::chrono::system_clock::time_point::min();
108
	}
109 110

private:
111
	void VisitServer(const ContentDirectoryService &server,
112
			 std::forward_list<std::string> &&vpath,
113 114 115
			 const DatabaseSelection &selection,
			 VisitDirectory visit_directory,
			 VisitSong visit_song,
116
			 VisitPlaylist visit_playlist) const;
117 118 119 120 121

	/**
	 * Run an UPnP search according to MPD parameters, and
	 * visit_song the results.
	 */
122
	void SearchSongs(const ContentDirectoryService &server,
123 124
			 const char *objid,
			 const DatabaseSelection &selection,
125
			 VisitSong visit_song) const;
126

127 128 129
	UPnPDirContent SearchSongs(const ContentDirectoryService &server,
				   const char *objid,
				   const DatabaseSelection &selection) const;
130

131
	UPnPDirObject Namei(const ContentDirectoryService &server,
132
			    std::forward_list<std::string> &&vpath) const;
133 134 135 136

	/**
	 * Take server and objid, return metadata.
	 */
137 138
	UPnPDirObject ReadNode(const ContentDirectoryService &server,
			       const char *objid) const;
139 140 141 142 143 144

	/**
	 * 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).
	 */
145 146
	std::string BuildPath(const ContentDirectoryService &server,
			      const UPnPDirObject& dirent) const;
147 148 149
};

Database *
150
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
151
		     gcc_unused DatabaseListener &listener,
152
		     const ConfigBlock &) noexcept
153
{
154
	return new UpnpDatabase(io_event_loop);
155 156
}

157 158
void
UpnpDatabase::Open()
159
{
160
	handle = UpnpClientGlobalInit();
161

162
	discovery = new UPnPDeviceDirectory(event_loop, handle);
163 164 165
	try {
		discovery->Start();
	} catch (...) {
166
		delete discovery;
167
		UpnpClientGlobalFinish();
168
		throw;
169 170 171 172
	}
}

void
173
UpnpDatabase::Close() noexcept
174
{
175
	delete discovery;
176
	UpnpClientGlobalFinish();
177 178 179
}

void
180
UpnpDatabase::ReturnSong(const LightSong *_song) const noexcept
181
{
182
	assert(_song != nullptr);
183

184 185
	UpnpSong *song = (UpnpSong *)const_cast<LightSong *>(_song);
	delete song;
186 187 188 189
}

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

198
	auto server = discovery->GetServer(vpath.front().c_str());
199 200
	vpath.pop_front();

201 202 203 204
	if (vpath.empty())
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");

205
	UPnPDirObject dirent;
206
	if (vpath.front() != rootid) {
207
		dirent = Namei(server, std::move(vpath));
208
	} else {
209 210 211 212 213 214
		vpath.pop_front();
		if (vpath.empty())
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such song");

		dirent = ReadNode(server, vpath.front().c_str());
215 216
	}

217
	return new UpnpSong(std::move(dirent), uri);
218 219 220 221 222 223
}

/**
 * Double-quote a string, adding internal backslash escaping.
 */
static void
224
dquote(std::string &out, const char *in) noexcept
225
{
226
	out.push_back('"');
227 228 229 230 231

	for (; *in != 0; ++in) {
		switch(*in) {
		case '\\':
		case '"':
232
			out.push_back('\\');
233 234
			break;
		}
235 236

		out.push_back(*in);
237 238
	}

239
	out.push_back('"');
240 241 242 243
}

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

253
	const auto searchcaps = server.getSearchCapabilities(handle);
254
	if (searchcaps.empty())
255
		return UPnPDirContent();
256 257 258

	std::string cond;
	for (const auto &item : filter->GetItems()) {
259 260 261
		if (auto t = dynamic_cast<const TagSongFilter *>(item.get())) {
			auto tag = t->GetTagType();
			if (tag == TAG_NUM_OF_ITEM_TYPES) {
262 263 264
				if (!cond.empty()) {
					cond += " and ";
				}
265
				cond += '(';
266 267 268 269 270 271 272
				bool first(true);
				for (const auto& cap : searchcaps) {
					if (first)
						first = false;
					else
						cond += " or ";
					cond += cap;
273
					if (t->GetFoldCase()) {
274 275 276 277
						cond += " contains ";
					} else {
						cond += " = ";
					}
278
					dquote(cond, t->GetValue().c_str());
279
				}
280
				cond += ')';
281
				continue;
282 283 284 285 286
			}

			if (tag == TAG_ALBUM_ARTIST)
				tag = TAG_ARTIST;

287
			const char *name = tag_table_lookup(upnp_tags, tag);
288 289 290 291 292 293 294 295 296 297 298 299 300
			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) */
301
			if (t->GetFoldCase()) {
302 303 304 305
				cond += " contains ";
			} else {
				cond += " = ";
			}
306
			dquote(cond, t->GetValue().c_str());
307
		}
308 309

		// TODO: support other ISongFilter implementations
310 311
	}

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

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

323
	LightSong song(path, meta.tag);
324 325
	song.real_uri = meta.url.c_str();

326 327
	if (selection.Match(song))
		visit_song(song);
328 329 330 331 332 333 334
}

/**
 * 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.
 */
335
static std::string
336
songPath(const std::string &servername,
337
	 const std::string &objid) noexcept
338 339 340 341
{
	return servername + "/" + rootid + "/" + objid;
}

342
void
343
UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
344 345
			  const char *objid,
			  const DatabaseSelection &selection,
346
			  VisitSong visit_song) const
347 348
{
	if (!visit_song)
349
		return;
350

351
	for (auto &dirent : SearchSongs(server, objid, selection).objects) {
352 353 354 355
		if (dirent.type != UPnPDirObject::Type::ITEM ||
		    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
			continue;

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

379
UPnPDirObject
380
UpnpDatabase::ReadNode(const ContentDirectoryService &server,
381
		       const char *objid) const
382
{
383
	auto dirbuf = server.getMetadata(handle, objid);
384 385
	if (dirbuf.objects.size() != 1)
		throw std::runtime_error("Bad resource");
386

387
	return std::move(dirbuf.objects.front());
388 389
}

390
std::string
391
UpnpDatabase::BuildPath(const ContentDirectoryService &server,
392
			const UPnPDirObject& idirent) const
393
{
394
	const char *pid = idirent.id.c_str();
395
	std::string path;
396
	while (strcmp(pid, rootid) != 0) {
397
		auto dirent = ReadNode(server, pid);
398
		pid = dirent.parent_id.c_str();
399 400 401 402 403 404

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

407
	return PathTraitsUTF8::Build(server.getFriendlyName(),
408
				     path.c_str());
409 410 411
}

// Take server and internal title pathname and return objid and metadata.
412
UPnPDirObject
413
UpnpDatabase::Namei(const ContentDirectoryService &server,
414
		    std::forward_list<std::string> &&vpath) const
415
{
416
	if (vpath.empty())
417
		// looking for root info
418
		return ReadNode(server, rootid);
419

420 421
	std::string objid(rootid);

422
	// Walk the path elements, read each directory and try to find the next one
423
	while (true) {
424
		auto dirbuf = server.readDir(handle, objid.c_str());
425 426

		// Look for the name in the sub-container list
427
		UPnPDirObject *child = dirbuf.FindObject(vpath.front().c_str());
428 429 430
		if (child == nullptr)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such object");
431

432 433
		vpath.pop_front();
		if (vpath.empty())
434
			return std::move(*child);
435

436 437 438
		if (child->type != UPnPDirObject::Type::CONTAINER)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not a container");
439

440
		objid = std::move(child->id);
441 442 443
	}
}

444
static void
445 446
VisitItem(const UPnPDirObject &object, const char *uri,
	  const DatabaseSelection &selection,
447
	  VisitSong visit_song, VisitPlaylist visit_playlist)
448 449 450 451 452
{
	assert(object.type == UPnPDirObject::Type::ITEM);

	switch (object.item_class) {
	case UPnPDirObject::ItemClass::MUSIC:
453 454
		visitSong(object, uri, selection, visit_song);
		break;
455 456 457 458 459 460 461 462 463 464 465

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

466
		break;
467 468

	case UPnPDirObject::ItemClass::UNKNOWN:
469
		break;
470 471 472
	}
}

473
static void
474 475 476 477
VisitObject(const UPnPDirObject &object, const char *uri,
	    const DatabaseSelection &selection,
	    VisitDirectory visit_directory,
	    VisitSong visit_song,
478
	    VisitPlaylist visit_playlist)
479 480 481 482 483 484 485
{
	switch (object.type) {
	case UPnPDirObject::Type::UNKNOWN:
		assert(false);
		gcc_unreachable();

	case UPnPDirObject::Type::CONTAINER:
486
		if (visit_directory)
487 488
			visit_directory(LightDirectory(uri,
						       std::chrono::system_clock::time_point::min()));
489
		break;
490 491

	case UPnPDirObject::Type::ITEM:
492 493 494
		VisitItem(object, uri, selection,
			  visit_song, visit_playlist);
		break;
495 496 497
	}
}

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

521 522 523
		const std::string objid(std::move(vpath.front()));
		vpath.pop_front();
		if (!vpath.empty())
524 525
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not found");
526

527
		if (visit_song) {
528
			auto dirent = ReadNode(server, objid.c_str());
529

530
			if (dirent.type != UPnPDirObject::Type::ITEM ||
531 532 533
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
						    "Not found");
534

535
			std::string path = songPath(server.getFriendlyName(),
536
						    dirent.id);
537 538
			visitSong(std::move(dirent), path.c_str(),
				  selection, visit_song);
539
		}
540 541

		return;
542 543 544
	}

	// Translate the target path into an object id and the associated metadata.
545
	const auto tdirent = Namei(server, std::move(vpath));
546 547 548 549 550

	/* 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. */
551 552 553 554
	if (selection.recursive && selection.filter) {
		SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
		return;
	}
555

Max Kellermann's avatar
Max Kellermann committed
556 557 558 559
	const char *const base_uri = selection.uri.empty()
		? server.getFriendlyName()
		: selection.uri.c_str();

560
	if (tdirent.type == UPnPDirObject::Type::ITEM) {
561 562 563 564
		VisitItem(tdirent, base_uri,
			  selection,
			  visit_song, visit_playlist);
		return;
565 566 567 568 569
	}

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

580 581 582 583 584 585 586 587 588
gcc_const
static DatabaseSelection
CheckSelection(DatabaseSelection selection) noexcept
{
	selection.uri.clear();
	selection.filter = nullptr;
	return selection;
}

589
// Deal with the possibly multiple servers, call VisitServer if needed.
590
void
591 592 593
UpnpDatabase::Visit(const DatabaseSelection &selection,
		    VisitDirectory visit_directory,
		    VisitSong visit_song,
594
		    VisitPlaylist visit_playlist) const
595
{
596 597
	DatabaseVisitorHelper helper(CheckSelection(selection), visit_song);

598
	auto vpath = SplitString(selection.uri.c_str(), '/');
599
	if (vpath.empty()) {
600
		for (const auto &server : discovery->GetDirectories()) {
601
			if (visit_directory) {
602 603
				const LightDirectory d(server.getFriendlyName(),
						       std::chrono::system_clock::time_point::min());
604
				visit_directory(d);
605
			}
606

607
			if (selection.recursive)
608
				VisitServer(server, std::move(vpath), selection,
609 610
					    visit_directory, visit_song,
					    visit_playlist);
611
		}
612

613
		helper.Commit();
614
		return;
615 616 617
	}

	// We do have a path: the first element selects the server
618
	std::string servername(std::move(vpath.front()));
619
	vpath.pop_front();
620

621
	auto server = discovery->GetServer(servername.c_str());
622
	VisitServer(server, std::move(vpath), selection,
623
		    visit_directory, visit_song, visit_playlist);
624
	helper.Commit();
625 626
}

627
void
628
UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
629
			      TagType tag, gcc_unused TagMask group_mask,
630
			      VisitTag visit_tag) const
631
{
632 633 634
	// TODO: use group_mask

	if (!visit_tag)
635
		return;
636 637

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

641 642 643 644 645
		for (const auto &dirent : dirbuf.objects) {
			if (dirent.type != UPnPDirObject::Type::ITEM ||
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				continue;

646
			const char *value = dirent.tag.GetValue(tag);
647 648
			if (value != nullptr) {
				values.emplace(value);
649
			}
650 651 652
		}
	}

653 654 655
	for (const auto& value : values) {
		TagBuilder builder;
		builder.AddItem(tag, value.c_str());
656
		visit_tag(builder.Commit());
657
	}
658 659
}

660 661
DatabaseStats
UpnpDatabase::GetStats(const DatabaseSelection &) const
662 663 664
{
	/* Note: this gets called before the daemonizing so we can't
	   reallyopen this would be a problem if we had real stats */
665
	DatabaseStats stats;
666
	stats.Clear();
667
	return stats;
668 669 670 671
}

const DatabasePlugin upnp_db_plugin = {
	"upnp",
672
	0,
673 674
	UpnpDatabase::Create,
};