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 52 53 54 55 56 57 58 59
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)) {}
};
60

61 62
class UpnpSong : UpnpSongData, public LightSong {
	std::string real_uri2;
63 64

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

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

public:
79 80 81
	explicit UpnpDatabase(EventLoop &_event_loop)
		:Database(upnp_db_plugin),
		 event_loop(_event_loop) {}
82

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

88 89 90
	void Open() override;
	void Close() override;
	const LightSong *GetSong(const char *uri_utf8) const override;
91
	void ReturnSong(const LightSong *song) const override;
92

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

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

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

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

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

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

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

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

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

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

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

155 156
void
UpnpDatabase::Open()
157
{
158
	handle = UpnpClientGlobalInit();
159

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

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

void
178
UpnpDatabase::ReturnSong(const LightSong *_song) const
179
{
180
	assert(_song != nullptr);
181

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

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

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

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

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

		dirent = ReadNode(server, vpath.front().c_str());
213 214
	}

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

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

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

		out.push_back(*in);
235 236
	}

237
	out.push_back('"');
238 239 240 241
}

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

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

	std::string cond;
	for (const auto &item : filter->GetItems()) {
		switch (auto tag = item.GetTag()) {
		case LOCATE_TAG_ANY_TYPE:
			{
				if (!cond.empty()) {
					cond += " and ";
				}
263
				cond += '(';
264 265 266 267 268 269 270 271 272 273 274 275
				bool first(true);
				for (const auto& cap : searchcaps) {
					if (first)
						first = false;
					else
						cond += " or ";
					cond += cap;
					if (item.GetFoldCase()) {
						cond += " contains ";
					} else {
						cond += " = ";
					}
276
					dquote(cond, item.GetValue());
277
				}
278
				cond += ')';
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 309 310 311
			}
			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 += " = ";
			}
312
			dquote(cond, item.GetValue());
313 314 315
		}
	}

316
	return server.search(handle, objid, cond.c_str());
317 318
}

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

327
	LightSong song(path, meta.tag);
328 329
	song.real_uri = meta.url.c_str();

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

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

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

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

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

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

391
	return std::move(dirbuf.objects.front());
392 393
}

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

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

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

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

424 425
	std::string objid(rootid);

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

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

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

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

444
		objid = std::move(child->id);
445 446 447
	}
}

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

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

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

470
		break;
471 472

	case UPnPDirObject::ItemClass::UNKNOWN:
473
		break;
474 475 476
	}
}

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

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

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

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

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

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

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

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

		return;
546 547 548
	}

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

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

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

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

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

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

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

606
		return;
607 608 609
	}

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

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

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

	if (!visit_tag)
626
		return;
627 628

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

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

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

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

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

const DatabasePlugin upnp_db_plugin = {
	"upnp",
663
	0,
664 665
	UpnpDatabase::Create,
};