1 /*
2 * Copyright 2003-2021 The Music Player Daemon Project
3 * http://www.musicpd.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "UpnpDatabasePlugin.hxx"
21 #include "Directory.hxx"
22 #include "Tags.hxx"
23 #include "lib/upnp/ClientInit.hxx"
24 #include "lib/upnp/Discovery.hxx"
25 #include "lib/upnp/ContentDirectoryService.hxx"
26 #include "db/Interface.hxx"
27 #include "db/DatabasePlugin.hxx"
28 #include "db/Selection.hxx"
29 #include "db/VHelper.hxx"
30 #include "db/UniqueTags.hxx"
31 #include "db/DatabaseError.hxx"
32 #include "db/LightDirectory.hxx"
33 #include "song/LightSong.hxx"
34 #include "song/Filter.hxx"
35 #include "song/TagSongFilter.hxx"
36 #include "db/Stats.hxx"
37 #include "tag/Table.hxx"
38 #include "fs/Traits.hxx"
39 #include "util/ConstBuffer.hxx"
40 #include "util/RecursiveMap.hxx"
41 #include "util/SplitString.hxx"
42 #include "config/Block.hxx"
43
44 #include <cassert>
45 #include <string>
46 #include <utility>
47
48 #include <string.h>
49
50 static const char *const rootid = "0";
51
52 class UpnpSongData {
53 protected:
54 std::string uri;
55 Tag tag;
56
57 template<typename U, typename T>
UpnpSongData(U && _uri,T && _tag)58 UpnpSongData(U &&_uri, T &&_tag) noexcept
59 :uri(std::forward<U>(_uri)), tag(std::forward<T>(_tag)) {}
60 };
61
62 class UpnpSong : UpnpSongData, public LightSong {
63 std::string real_uri2;
64
65 public:
66 template<typename U>
UpnpSong(UPnPDirObject && object,U && _uri)67 UpnpSong(UPnPDirObject &&object, U &&_uri) noexcept
68 :UpnpSongData(std::forward<U>(_uri), std::move(object.tag)),
69 LightSong(UpnpSongData::uri.c_str(), UpnpSongData::tag),
70 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 const char* interface;
81
82 public:
UpnpDatabase(EventLoop & _event_loop,const ConfigBlock & block)83 explicit UpnpDatabase(EventLoop &_event_loop, const ConfigBlock &block) noexcept
84 :Database(upnp_db_plugin),
85 event_loop(_event_loop),
86 interface(block.GetBlockValue("interface", nullptr)) {}
87
88 static DatabasePtr Create(EventLoop &main_event_loop,
89 EventLoop &io_event_loop,
90 DatabaseListener &listener,
91 const ConfigBlock &block) noexcept;
92
93 void Open() override;
94 void Close() noexcept override;
95 [[nodiscard]] const LightSong *GetSong(std::string_view uri_utf8) const override;
96 void ReturnSong(const LightSong *song) const noexcept override;
97
98 void Visit(const DatabaseSelection &selection,
99 VisitDirectory visit_directory,
100 VisitSong visit_song,
101 VisitPlaylist visit_playlist) const override;
102
103 [[nodiscard]] RecursiveMap<std::string> CollectUniqueTags(const DatabaseSelection &selection,
104 ConstBuffer<TagType> tag_types) const override;
105
106 [[nodiscard]] DatabaseStats GetStats(const DatabaseSelection &selection) const override;
107
GetUpdateStamp() const108 [[nodiscard]] std::chrono::system_clock::time_point GetUpdateStamp() const noexcept override {
109 return std::chrono::system_clock::time_point::min();
110 }
111
112 private:
113 void VisitServer(const ContentDirectoryService &server,
114 std::forward_list<std::string_view> &&vpath,
115 const DatabaseSelection &selection,
116 const VisitDirectory& visit_directory,
117 const VisitSong& visit_song,
118 const VisitPlaylist& visit_playlist) const;
119
120 /**
121 * Run an UPnP search according to MPD parameters, and
122 * visit_song the results.
123 */
124 void SearchSongs(const ContentDirectoryService &server,
125 const char *objid,
126 const DatabaseSelection &selection,
127 const VisitSong& visit_song) const;
128
129 UPnPDirContent SearchSongs(const ContentDirectoryService &server,
130 const char *objid,
131 const DatabaseSelection &selection) const;
132
133 UPnPDirObject Namei(const ContentDirectoryService &server,
134 std::forward_list<std::string_view> &&vpath) const;
135
136 /**
137 * Take server and objid, return metadata.
138 */
139 UPnPDirObject ReadNode(const ContentDirectoryService &server,
140 const char *objid) const;
141
142 /**
143 * Get the path for an object Id. This works much like pwd,
144 * except easier cause our inodes have a parent id. Not used
145 * any more actually (see comments in SearchSongs).
146 */
147 [[nodiscard]] std::string BuildPath(const ContentDirectoryService &server,
148 const UPnPDirObject& dirent) const;
149 };
150
151 DatabasePtr
Create(EventLoop &,EventLoop & io_event_loop,DatabaseListener & listener,const ConfigBlock & block)152 UpnpDatabase::Create(EventLoop &, EventLoop &io_event_loop,
153 [[maybe_unused]] DatabaseListener &listener,
154 const ConfigBlock &block) noexcept
155 {
156 return std::make_unique<UpnpDatabase>(io_event_loop, block);;
157 }
158
159 void
Open()160 UpnpDatabase::Open()
161 {
162 handle = UpnpClientGlobalInit(interface);
163
164 discovery = new UPnPDeviceDirectory(event_loop, handle);
165 try {
166 discovery->Start();
167 } catch (...) {
168 delete discovery;
169 UpnpClientGlobalFinish();
170 throw;
171 }
172 }
173
174 void
Close()175 UpnpDatabase::Close() noexcept
176 {
177 delete discovery;
178 UpnpClientGlobalFinish();
179 }
180
181 void
ReturnSong(const LightSong * _song) const182 UpnpDatabase::ReturnSong(const LightSong *_song) const noexcept
183 {
184 assert(_song != nullptr);
185
186 auto *song = (UpnpSong *)const_cast<LightSong *>(_song);
187 delete song;
188 }
189
190 // Get song info by path. We can receive either the id path, or the titles
191 // one
192 const LightSong *
GetSong(std::string_view uri) const193 UpnpDatabase::GetSong(std::string_view uri) const
194 {
195 auto vpath = SplitString(uri, '/');
196 if (vpath.empty())
197 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
198 "No such song");
199
200 auto server = discovery->GetServer(vpath.front());
201 vpath.pop_front();
202
203 if (vpath.empty())
204 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
205 "No such song");
206
207 UPnPDirObject dirent;
208 if (vpath.front() != rootid) {
209 dirent = Namei(server, std::move(vpath));
210 } else {
211 vpath.pop_front();
212 if (vpath.empty())
213 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
214 "No such song");
215
216 dirent = ReadNode(server, std::string(vpath.front()).c_str());
217 }
218
219 return new UpnpSong(std::move(dirent), uri);
220 }
221
222 /**
223 * Double-quote a string, adding internal backslash escaping.
224 */
225 static void
dquote(std::string & out,const char * in)226 dquote(std::string &out, const char *in) noexcept
227 {
228 out.push_back('"');
229
230 for (; *in != 0; ++in) {
231 switch(*in) {
232 case '\\':
233 case '"':
234 out.push_back('\\');
235 break;
236 }
237
238 out.push_back(*in);
239 }
240
241 out.push_back('"');
242 }
243
244 // Run an UPnP search, according to MPD parameters. Return results as
245 // UPnP items
246 UPnPDirContent
SearchSongs(const ContentDirectoryService & server,const char * objid,const DatabaseSelection & selection) const247 UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
248 const char *objid,
249 const DatabaseSelection &selection) const
250 {
251 const SongFilter *filter = selection.filter;
252 if (selection.filter == nullptr)
253 return {};
254
255 const auto searchcaps = server.getSearchCapabilities(handle);
256 if (searchcaps.empty())
257 return {};
258
259 std::string cond;
260 for (const auto &item : filter->GetItems()) {
261 if (auto t = dynamic_cast<const TagSongFilter *>(item.get())) {
262 auto tag = t->GetTagType();
263 if (tag == TAG_NUM_OF_ITEM_TYPES) {
264 if (!cond.empty()) {
265 cond += " and ";
266 }
267 cond += '(';
268 bool first(true);
269 for (const auto& cap : searchcaps) {
270 if (first)
271 first = false;
272 else
273 cond += " or ";
274 cond += cap;
275 if (t->GetFoldCase()) {
276 cond += " contains ";
277 } else {
278 cond += " = ";
279 }
280 dquote(cond, t->GetValue().c_str());
281 }
282 cond += ')';
283 continue;
284 }
285
286 if (tag == TAG_ALBUM_ARTIST)
287 tag = TAG_ARTIST;
288
289 const char *name = tag_table_lookup(upnp_tags, tag);
290 if (name == nullptr)
291 continue;
292
293 if (!cond.empty()) {
294 cond += " and ";
295 }
296 cond += name;
297
298 /* FoldCase doubles up as contains/equal
299 switch. UpNP search is supposed to be
300 case-insensitive, but at least some servers
301 have the same convention as mpd (e.g.:
302 minidlna) */
303 if (t->GetFoldCase()) {
304 cond += " contains ";
305 } else {
306 cond += " = ";
307 }
308 dquote(cond, t->GetValue().c_str());
309 }
310
311 // TODO: support other ISongFilter implementations
312 }
313
314 return server.search(handle, objid, cond.c_str());
315 }
316
317 static void
visitSong(const UPnPDirObject & meta,const char * path,const DatabaseSelection & selection,const VisitSong & visit_song)318 visitSong(const UPnPDirObject &meta, const char *path,
319 const DatabaseSelection &selection,
320 const VisitSong& visit_song)
321 {
322 if (!visit_song)
323 return;
324
325 LightSong song(path, meta.tag);
326 song.real_uri = meta.url.c_str();
327
328 if (selection.Match(song))
329 visit_song(song);
330 }
331
332 /**
333 * Build synthetic path based on object id for search results. The use
334 * of "rootid" is arbitrary, any name that is not likely to be a top
335 * directory name would fit.
336 */
337 static std::string
songPath(const std::string & servername,const std::string & objid)338 songPath(const std::string &servername,
339 const std::string &objid) noexcept
340 {
341 return servername + "/" + rootid + "/" + objid;
342 }
343
344 void
SearchSongs(const ContentDirectoryService & server,const char * objid,const DatabaseSelection & selection,const VisitSong & visit_song) const345 UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
346 const char *objid,
347 const DatabaseSelection &selection,
348 const VisitSong& visit_song) const
349 {
350 if (!visit_song)
351 return;
352
353 const auto content = SearchSongs(server, objid, selection);
354 for (auto &dirent : content.objects) {
355 if (dirent.type != UPnPDirObject::Type::ITEM ||
356 dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
357 continue;
358
359 // We get song ids as the result of the UPnP search. But our
360 // client expects paths (e.g. we get 1$4$3788 from minidlna,
361 // but we need to translate to /Music/All_Music/Satisfaction).
362 // We can do this in two ways:
363 // - Rebuild a normal path using BuildPath() which is a kind of pwd
364 // - Build a bogus path based on the song id.
365 // The first method is nice because the returned paths are pretty, but
366 // it has two big problems:
367 // - The song paths are ambiguous: e.g. minidlna returns all search
368 // results as being from the "All Music" directory, which can
369 // contain several songs with the same title (but different objids)
370 // - The performance of BuildPath() is atrocious on very big
371 // directories, even causing timeouts in clients. And of
372 // course, 'All Music' is very big.
373 // So we return synthetic and ugly paths based on the object id,
374 // which we later have to detect.
375 const std::string path = songPath(server.getFriendlyName(),
376 dirent.id);
377 visitSong(dirent, path.c_str(),
378 selection, visit_song);
379 }
380 }
381
382 UPnPDirObject
ReadNode(const ContentDirectoryService & server,const char * objid) const383 UpnpDatabase::ReadNode(const ContentDirectoryService &server,
384 const char *objid) const
385 {
386 auto dirbuf = server.getMetadata(handle, objid);
387 if (dirbuf.objects.size() != 1)
388 throw std::runtime_error("Bad resource");
389
390 return std::move(dirbuf.objects.front());
391 }
392
393 std::string
BuildPath(const ContentDirectoryService & server,const UPnPDirObject & idirent) const394 UpnpDatabase::BuildPath(const ContentDirectoryService &server,
395 const UPnPDirObject& idirent) const
396 {
397 const char *pid = idirent.id.c_str();
398 std::string path;
399 while (strcmp(pid, rootid) != 0) {
400 auto dirent = ReadNode(server, pid);
401 pid = dirent.parent_id.c_str();
402
403 if (path.empty())
404 path = dirent.name;
405 else
406 path = PathTraitsUTF8::Build(dirent.name, path);
407 }
408
409 return PathTraitsUTF8::Build(server.getFriendlyName(),
410 path.c_str());
411 }
412
413 // Take server and internal title pathname and return objid and metadata.
414 UPnPDirObject
Namei(const ContentDirectoryService & server,std::forward_list<std::string_view> && vpath) const415 UpnpDatabase::Namei(const ContentDirectoryService &server,
416 std::forward_list<std::string_view> &&vpath) const
417 {
418 if (vpath.empty())
419 // looking for root info
420 return ReadNode(server, rootid);
421
422 std::string objid(rootid);
423
424 // Walk the path elements, read each directory and try to find the next one
425 while (true) {
426 auto dirbuf = server.readDir(handle, objid.c_str());
427
428 // Look for the name in the sub-container list
429 UPnPDirObject *child = dirbuf.FindObject(vpath.front());
430 if (child == nullptr)
431 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
432 "No such object");
433
434 vpath.pop_front();
435 if (vpath.empty())
436 return std::move(*child);
437
438 if (child->type != UPnPDirObject::Type::CONTAINER)
439 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
440 "Not a container");
441
442 objid = std::move(child->id);
443 }
444 }
445
446 static void
VisitItem(const UPnPDirObject & object,const char * uri,const DatabaseSelection & selection,const VisitSong & visit_song,const VisitPlaylist & visit_playlist)447 VisitItem(const UPnPDirObject &object, const char *uri,
448 const DatabaseSelection &selection,
449 const VisitSong& visit_song, const VisitPlaylist& visit_playlist)
450 {
451 assert(object.type == UPnPDirObject::Type::ITEM);
452
453 switch (object.item_class) {
454 case UPnPDirObject::ItemClass::MUSIC:
455 visitSong(object, uri, selection, visit_song);
456 break;
457
458 case UPnPDirObject::ItemClass::PLAYLIST:
459 if (visit_playlist) {
460 /* Note: I've yet to see a
461 playlist item (playlists
462 seem to be usually handled
463 as containers, so I'll
464 decide what to do when I
465 see one... */
466 }
467
468 break;
469
470 case UPnPDirObject::ItemClass::UNKNOWN:
471 break;
472 }
473 }
474
475 static void
VisitObject(const UPnPDirObject & object,const char * uri,const DatabaseSelection & selection,const VisitDirectory & visit_directory,const VisitSong & visit_song,const VisitPlaylist & visit_playlist)476 VisitObject(const UPnPDirObject &object, const char *uri,
477 const DatabaseSelection &selection,
478 const VisitDirectory& visit_directory,
479 const VisitSong& visit_song,
480 const VisitPlaylist& visit_playlist)
481 {
482 switch (object.type) {
483 case UPnPDirObject::Type::UNKNOWN:
484 assert(false);
485 gcc_unreachable();
486
487 case UPnPDirObject::Type::CONTAINER:
488 if (visit_directory)
489 visit_directory(LightDirectory(uri,
490 std::chrono::system_clock::time_point::min()));
491 break;
492
493 case UPnPDirObject::Type::ITEM:
494 VisitItem(object, uri, selection,
495 visit_song, visit_playlist);
496 break;
497 }
498 }
499
500 // vpath is a parsed and writeable version of selection.uri. There is
501 // really just one path parameter.
502 void
VisitServer(const ContentDirectoryService & server,std::forward_list<std::string_view> && vpath,const DatabaseSelection & selection,const VisitDirectory & visit_directory,const VisitSong & visit_song,const VisitPlaylist & visit_playlist) const503 UpnpDatabase::VisitServer(const ContentDirectoryService &server,
504 std::forward_list<std::string_view> &&vpath,
505 const DatabaseSelection &selection,
506 const VisitDirectory& visit_directory,
507 const VisitSong& visit_song,
508 const VisitPlaylist& visit_playlist) const
509 {
510 /* If the path begins with rootid, we know that this is a
511 song, not a directory (because that's how we set things
512 up). Just visit it. Note that the choice of rootid is
513 arbitrary, any value not likely to be the name of a top
514 directory would be ok. */
515 /* !Note: this *can't* be handled by Namei further down,
516 because the path is not valid for traversal. Besides, it's
517 just faster to access the target node directly */
518 if (!vpath.empty() && vpath.front() == rootid) {
519 vpath.pop_front();
520 if (vpath.empty())
521 return;
522
523 const std::string objid(vpath.front());
524 vpath.pop_front();
525 if (!vpath.empty())
526 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
527 "Not found");
528
529 if (visit_song) {
530 auto dirent = ReadNode(server, objid.c_str());
531
532 if (dirent.type != UPnPDirObject::Type::ITEM ||
533 dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
534 throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
535 "Not found");
536
537 std::string path = songPath(server.getFriendlyName(),
538 dirent.id);
539 visitSong(dirent, path.c_str(),
540 selection, visit_song);
541 }
542
543 return;
544 }
545
546 // Translate the target path into an object id and the associated metadata.
547 const auto tdirent = Namei(server, std::move(vpath));
548
549 /* If recursive is set, this is a search... No use sending it
550 if the filter is empty. In this case, we implement limited
551 recursion (1-deep) here, which will handle the "add dir"
552 case. */
553 if (selection.recursive && selection.filter) {
554 SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
555 return;
556 }
557
558 const char *const base_uri = selection.uri.empty()
559 ? server.getFriendlyName()
560 : selection.uri.c_str();
561
562 if (tdirent.type == UPnPDirObject::Type::ITEM) {
563 VisitItem(tdirent, base_uri,
564 selection,
565 visit_song, visit_playlist);
566 return;
567 }
568
569 /* Target was a a container. Visit it. We could read slices
570 and loop here, but it's not useful as mpd will only return
571 data to the client when we're done anyway. */
572 const auto contents = server.readDir(handle, tdirent.id.c_str());
573 for (const auto &dirent : contents.objects) {
574 const std::string uri = PathTraitsUTF8::Build(base_uri,
575 dirent.name.c_str());
576 VisitObject(dirent, uri.c_str(),
577 selection,
578 visit_directory,
579 visit_song, visit_playlist);
580 }
581 }
582
583 gcc_const
584 static DatabaseSelection
CheckSelection(DatabaseSelection selection)585 CheckSelection(DatabaseSelection selection) noexcept
586 {
587 selection.uri.clear();
588 selection.filter = nullptr;
589 return selection;
590 }
591
592 // Deal with the possibly multiple servers, call VisitServer if needed.
593 void
Visit(const DatabaseSelection & selection,VisitDirectory visit_directory,VisitSong visit_song,VisitPlaylist visit_playlist) const594 UpnpDatabase::Visit(const DatabaseSelection &selection,
595 VisitDirectory visit_directory,
596 VisitSong visit_song,
597 VisitPlaylist visit_playlist) const
598 {
599 DatabaseVisitorHelper helper(CheckSelection(selection), visit_song);
600
601 auto vpath = SplitString(selection.uri, '/');
602 if (vpath.empty()) {
603 for (const auto &server : discovery->GetDirectories()) {
604 if (visit_directory) {
605 const LightDirectory d(server.getFriendlyName(),
606 std::chrono::system_clock::time_point::min());
607 visit_directory(d);
608 }
609
610 if (selection.recursive)
611 VisitServer(server, std::move(vpath), selection,
612 visit_directory, visit_song,
613 visit_playlist);
614 }
615
616 helper.Commit();
617 return;
618 }
619
620 // We do have a path: the first element selects the server
621 std::string servername(vpath.front());
622 vpath.pop_front();
623
624 auto server = discovery->GetServer(servername.c_str());
625 VisitServer(server, std::move(vpath), selection,
626 visit_directory, visit_song, visit_playlist);
627 helper.Commit();
628 }
629
630 RecursiveMap<std::string>
CollectUniqueTags(const DatabaseSelection & selection,ConstBuffer<TagType> tag_types) const631 UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
632 ConstBuffer<TagType> tag_types) const
633 {
634 return ::CollectUniqueTags(*this, selection, tag_types);
635 }
636
637 DatabaseStats
GetStats(const DatabaseSelection &) const638 UpnpDatabase::GetStats(const DatabaseSelection &) const
639 {
640 /* Note: this gets called before the daemonizing so we can't
641 reallyopen this would be a problem if we had real stats */
642 DatabaseStats stats;
643 stats.Clear();
644 return stats;
645 }
646
647 const DatabasePlugin upnp_db_plugin = {
648 "upnp",
649 0,
650 UpnpDatabase::Create,
651 };
652