UpnpDatabasePlugin.cxx 17.4 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 * 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 "UpnpDatabasePlugin.hxx"
21 22
#include "Directory.hxx"
#include "Tags.hxx"
23
#include "lib/upnp/ClientInit.hxx"
24 25
#include "lib/upnp/Discovery.hxx"
#include "lib/upnp/ContentDirectoryService.hxx"
26
#include "db/Interface.hxx"
Max Kellermann's avatar
Max Kellermann committed
27 28
#include "db/DatabasePlugin.hxx"
#include "db/Selection.hxx"
29
#include "db/VHelper.hxx"
30
#include "db/UniqueTags.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 "tag/Table.hxx"
38
#include "fs/Traits.hxx"
39 40
#include "util/ConstBuffer.hxx"
#include "util/RecursiveMap.hxx"
41
#include "util/SplitString.hxx"
42

43
#include <cassert>
44
#include <string>
45
#include <utility>
46 47 48 49 50

#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 66 67
	template<typename U>
	UpnpSong(UPnPDirObject &&object, U &&_uri) noexcept
		:UpnpSongData(std::forward<U>(_uri), std::move(object.tag)),
68 69
		 LightSong(UpnpSongData::uri.c_str(), UpnpSongData::tag),
		 real_uri2(std::move(object.url)) {
70
		real_uri = real_uri2.c_str();
71 72 73
	}
};

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

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

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

89
	void Open() override;
90
	void Close() noexcept override;
91
	[[nodiscard]] const LightSong *GetSong(std::string_view uri_utf8) const override;
92
	void ReturnSong(const LightSong *song) const noexcept override;
93

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

99
	[[nodiscard]] RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
100
						    ConstBuffer<TagType> tag_types) const override;
101

102
	[[nodiscard]] DatabaseStats GetStats(const DatabaseSelection &selection) const override;
103

104
	[[nodiscard]] 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_view> &&vpath,
111
			 const DatabaseSelection &selection,
112 113 114
			 const VisitDirectory& visit_directory,
			 const VisitSong& visit_song,
			 const 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
			 const 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_view> &&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
	[[nodiscard]] std::string BuildPath(const ContentDirectoryService &server,
144
			      const UPnPDirObject& dirent) const;
145 146
};

147
DatabasePtr
148
UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
Rosen Penev's avatar
Rosen Penev committed
149
		     [[maybe_unused]] DatabaseListener &listener,
150
		     const ConfigBlock &) noexcept
151
{
152
	return std::make_unique<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
	}
}

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

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

Max Kellermann's avatar
Max Kellermann committed
182
	auto *song = (UpnpSong *)const_cast<LightSong *>(_song);
183
	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(std::string_view 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());
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
		vpath.pop_front();
		if (vpath.empty())
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such song");

212
		dirent = ReadNode(server, std::string(vpath.front()).c_str());
213 214
	}

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

/**
 * Double-quote a string, adding internal backslash escaping.
 */
static void
222
dquote(std::string &out, const char *in) noexcept
223
{
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

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

			if (tag == TAG_ALBUM_ARTIST)
				tag = TAG_ARTIST;

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

		// TODO: support other ISongFilter implementations
308 309
	}

310
	return server.search(handle, objid, cond.c_str());
311 312
}

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

321
	LightSong song(path, meta.tag);
322 323
	song.real_uri = meta.url.c_str();

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

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

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

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

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

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

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

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

		if (path.empty())
			path = dirent.name;
		else
402
			path = PathTraitsUTF8::Build(dirent.name, path);
403
	}
404

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

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

418 419
	std::string objid(rootid);

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

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

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

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

438
		objid = std::move(child->id);
439 440 441
	}
}

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

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

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

464
		break;
465 466

	case UPnPDirObject::ItemClass::UNKNOWN:
467
		break;
468 469 470
	}
}

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

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

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

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

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

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

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

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

		return;
540 541 542
	}

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

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

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

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

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

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

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

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

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

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

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

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

626
RecursiveMap<std::string>
627
UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
628
				ConstBuffer<TagType> tag_types) const
629
{
630
	return ::CollectUniqueTags(*this, selection, tag_types);
631 632
}

633 634
DatabaseStats
UpnpDatabase::GetStats(const DatabaseSelection &) const
635 636 637
{
	/* Note: this gets called before the daemonizing so we can't
	   reallyopen this would be a problem if we had real stats */
638
	DatabaseStats stats;
639
	stats.Clear();
640
	return stats;
641 642 643 644
}

const DatabasePlugin upnp_db_plugin = {
	"upnp",
645
	0,
646 647
	UpnpDatabase::Create,
};