1 /*MT*
2 
3     MediaTomb - http://www.mediatomb.cc/
4 
5     fallback_layout.cc - this file is part of MediaTomb.
6 
7     Copyright (C) 2005 Gena Batyan <bgeradz@mediatomb.cc>,
8                        Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>
9 
10     Copyright (C) 2006-2010 Gena Batyan <bgeradz@mediatomb.cc>,
11                             Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>,
12                             Leonhard Wimmer <leo@mediatomb.cc>
13 
14     MediaTomb is free software; you can redistribute it and/or modify
15     it under the terms of the GNU General Public License version 2
16     as published by the Free Software Foundation.
17 
18     MediaTomb is distributed in the hope that it will be useful,
19     but WITHOUT ANY WARRANTY; without even the implied warranty of
20     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21     GNU General Public License for more details.
22 
23     You should have received a copy of the GNU General Public License
24     version 2 along with MediaTomb; if not, write to the Free Software
25     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
26 
27     $Id$
28 */
29 
30 /// \file builtin_layout.cc
31 
32 #include "builtin_layout.h" // API
33 
34 #include <regex>
35 
36 #include "config/config_manager.h"
37 #include "content/content_manager.h"
38 #include "metadata/metadata_handler.h"
39 #include "util/string_converter.h"
40 
41 #ifdef ONLINE_SERVICES
42 
43 #include "content/onlineservice/online_service.h"
44 
45 #ifdef SOPCAST
46 #include "content/onlineservice/sopcast_content_handler.h"
47 #endif
48 
49 #ifdef ATRAILERS
50 #include "content/onlineservice/atrailers_content_handler.h"
51 #endif
52 
53 #endif //ONLINE_SERVICES
54 
add(const std::shared_ptr<CdsObject> & obj,const std::pair<int,bool> & parentID,bool use_ref)55 void BuiltinLayout::add(const std::shared_ptr<CdsObject>& obj, const std::pair<int, bool>& parentID, bool use_ref)
56 {
57     obj->setParentID(parentID.first);
58     if (use_ref)
59         obj->setFlag(OBJECT_FLAG_USE_RESOURCE_REF);
60     obj->setID(INVALID_OBJECT_ID);
61 
62     content->addObject(obj, parentID.second);
63 }
64 
esc(std::string str)65 std::string BuiltinLayout::esc(std::string str)
66 {
67     return escape(std::move(str), VIRTUAL_CONTAINER_ESCAPE, VIRTUAL_CONTAINER_SEPARATOR);
68 }
69 
addVideo(const std::shared_ptr<CdsObject> & obj,const fs::path & rootpath)70 void BuiltinLayout::addVideo(const std::shared_ptr<CdsObject>& obj, const fs::path& rootpath)
71 {
72     auto f2i = StringConverter::f2i(config);
73     auto id = content->addContainerChain("/Video/All Video");
74 
75     if (obj->getID() != INVALID_OBJECT_ID) {
76         obj->setRefID(obj->getID());
77         add(obj, id);
78     } else {
79         add(obj, id);
80         obj->setRefID(obj->getID());
81     }
82 
83     auto meta = obj->getMetaData();
84 
85     std::string date = getValueOrDefault(meta, MetadataHandler::getMetaFieldName(M_CREATION_DATE));
86     if (!date.empty()) {
87         std::string year, month;
88         auto m = std::numeric_limits<std::size_t>::max();
89         auto y = date.find('-');
90         if (y != std::string::npos) {
91             year = date.substr(0, y);
92             month = date.substr(y + 1);
93             m = month.find('-');
94             if (m != std::string::npos)
95                 month = month.substr(0, m);
96         }
97 
98         std::string chain;
99         if ((y > 0) && (m > 0)) {
100             chain = fmt::format("/Video/Year/{}/{}", esc(year), esc(month));
101             id = content->addContainerChain(chain);
102             add(obj, id);
103         }
104 
105         chain = fmt::format("/Video/Date/{}", esc(date));
106         id = content->addContainerChain(chain);
107         add(obj, id);
108     }
109 
110     fs::path dir;
111     if (!rootpath.empty()) {
112         // make location relative to rootpath: "/home/.../Videos/Action/a.mkv" with rootpath "/home/.../Videos" -> "Action"
113         dir = fs::relative(obj->getLocation().parent_path(), config->getBoolOption(CFG_IMPORT_LAYOUT_PARENT_PATH) ? rootpath.parent_path() : rootpath);
114         dir = f2i->convert(dir);
115     } else
116         dir = esc(f2i->convert(getLastPath(obj->getLocation())));
117 
118     if (!dir.empty()) {
119         id = content->addContainerChain(fmt::format("/Video/Directories/{}", dir.string().c_str()));
120         add(obj, id);
121     }
122 }
123 
addImage(const std::shared_ptr<CdsObject> & obj,const fs::path & rootpath)124 void BuiltinLayout::addImage(const std::shared_ptr<CdsObject>& obj, const fs::path& rootpath)
125 {
126     auto f2i = StringConverter::f2i(config);
127 
128     auto id = content->addContainerChain("/Photos/All Photos");
129     if (obj->getID() != INVALID_OBJECT_ID) {
130         obj->setRefID(obj->getID());
131         add(obj, id);
132     } else {
133         add(obj, id);
134         obj->setRefID(obj->getID());
135     }
136 
137     auto meta = obj->getMetaData();
138 
139     std::string date = getValueOrDefault(meta, MetadataHandler::getMetaFieldName(M_DATE));
140     if (!date.empty()) {
141         std::string year, month;
142         auto m = std::numeric_limits<std::size_t>::max();
143         auto y = date.find('-');
144         if (y != std::string::npos) {
145             year = date.substr(0, y);
146             month = date.substr(y + 1);
147             m = month.find('-');
148             if (m != std::string::npos)
149                 month = month.substr(0, m);
150         }
151 
152         std::string chain;
153         if ((y > 0) && (m > 0)) {
154             chain = fmt::format("/Photos/Year/{}/{}", esc(year), esc(month));
155             id = content->addContainerChain(chain);
156             add(obj, id);
157         }
158 
159         chain = fmt::format("/Photos/Date/{}", esc(date));
160         id = content->addContainerChain(chain);
161         add(obj, id);
162     }
163 
164     fs::path dir;
165     if (!rootpath.empty()) {
166         // make location relative to rootpath: "/home/.../Photos/Action/a.mkv" with rootpath "/home/.../Photos" -> "Action"
167         dir = fs::relative(obj->getLocation().parent_path(), config->getBoolOption(CFG_IMPORT_LAYOUT_PARENT_PATH) ? rootpath.parent_path() : rootpath);
168         dir = f2i->convert(dir);
169     } else
170         dir = esc(f2i->convert(getLastPath(obj->getLocation())));
171 
172     if (!dir.empty()) {
173         id = content->addContainerChain(fmt::format("/Photos/Directories/{}", dir.string().c_str()));
174         add(obj, id);
175     }
176 }
177 
addAudio(const std::shared_ptr<CdsObject> & obj,const fs::path & rootpath)178 void BuiltinLayout::addAudio(const std::shared_ptr<CdsObject>& obj, const fs::path& rootpath)
179 {
180     auto f2i = StringConverter::f2i(config);
181 
182     std::string desc;
183 
184     std::string title = obj->getMetaData(M_TITLE);
185     if (title.empty())
186         title = obj->getTitle();
187 
188     std::string artist = obj->getMetaData(M_ARTIST);
189     std::string artist_full;
190     if (!artist.empty()) {
191         artist_full = artist;
192         desc = artist;
193     } else
194         artist = "Unknown";
195 
196     std::string album = obj->getMetaData(M_ALBUM);
197     std::string album_full;
198     if (!album.empty()) {
199         desc = fmt::format("{}, {}", desc, album);
200         album_full = album;
201     } else {
202         album = "Unknown";
203     }
204 
205     if (!desc.empty())
206         desc = fmt::format("{}, ", desc);
207     desc = fmt::format("{}{}", desc, title);
208 
209     std::string date = obj->getMetaData(M_DATE);
210     std::string albumDate;
211     if (!date.empty()) {
212         auto i = date.find('-');
213         if (i != std::string::npos)
214             date = date.substr(0, i);
215 
216         desc = fmt::format("{}, {}", desc, date);
217         albumDate = esc(date);
218     } else {
219         date = "Unknown";
220         albumDate = "Unknown";
221     }
222     obj->removeMetaData(M_UPNP_DATE);
223     obj->addMetaData(M_UPNP_DATE, albumDate);
224 
225     std::string genre = obj->getMetaData(M_GENRE);
226     if (!genre.empty()) {
227         genre = mapGenre(genre);
228         desc = fmt::format("{}, {}", desc, genre);
229     } else {
230         genre = "Unknown";
231     }
232 
233     std::string description = obj->getMetaData(M_DESCRIPTION);
234     if (description.empty()) {
235         obj->addMetaData(M_DESCRIPTION, desc);
236     }
237 
238     std::string composer = obj->getMetaData(M_COMPOSER);
239     if (composer.empty())
240         composer = "None";
241 
242     auto id = content->addContainerChain("/Audio/All Audio");
243     obj->setTitle(title);
244 
245     // we get the main object here, so the object that we will add below
246     // will be a reference of the main object, that's why we set the ref
247     // id to the object id - the add function will clear out the object
248     // id
249     if (obj->getID() != INVALID_OBJECT_ID) {
250         obj->setRefID(obj->getID());
251         add(obj, id);
252     } else {
253         // the object is not yet in the database (probably we got it from a
254         // playlist script, so we set the ref id after adding - it will be used
255         // for all consequent virtual objects
256         add(obj, id);
257         obj->setRefID(obj->getID());
258     }
259 
260     artist = esc(artist);
261 
262     std::string chain = fmt::format("/Audio/Artists/{}/All Songs", artist);
263 
264     id = content->addContainerChain(chain);
265     add(obj, id);
266 
267     std::string temp;
268     if (!artist_full.empty())
269         temp = artist_full;
270 
271     if (!album_full.empty())
272         temp = fmt::format("{} - {} - ", temp, album_full);
273     else
274         temp = fmt::format("{} - ", temp);
275 
276     album = esc(album);
277     chain = fmt::format("/Audio/Artists/{}/{}", artist, album);
278     id = content->addContainerChain(chain, UPNP_CLASS_MUSIC_ALBUM, obj->getID(), obj);
279     add(obj, id);
280 
281     chain = fmt::format("/Audio/Albums/{}", album);
282     id = content->addContainerChain(chain, UPNP_CLASS_MUSIC_ALBUM, obj->getID(), obj);
283     add(obj, id);
284 
285     chain = fmt::format("/Audio/Genres/{}", esc(genre));
286     id = content->addContainerChain(chain, UPNP_CLASS_MUSIC_GENRE);
287     add(obj, id);
288 
289     chain = fmt::format("/Audio/Composers/{}", esc(composer));
290     id = content->addContainerChain(chain, UPNP_CLASS_MUSIC_COMPOSER);
291     add(obj, id);
292 
293     chain = fmt::format("/Audio/Year/{}", esc(date));
294     id = content->addContainerChain(chain);
295     add(obj, id);
296 
297     obj->setTitle(fmt::format("{}{}", temp, title));
298 
299     id = content->addContainerChain("/Audio/All - full name");
300     add(obj, id);
301 
302     chain = fmt::format("/Audio/Artists/{}/All - full name", artist);
303     id = content->addContainerChain(chain);
304     add(obj, id);
305 
306     obj->setTitle(title);
307     fs::path dir;
308     if (!rootpath.empty()) {
309         // make location relative to rootpath: "/home/.../Audio/Action/a.mp3" with rootpath "/home/.../Audio" -> "Action"
310         dir = fs::relative(obj->getLocation().parent_path(), config->getBoolOption(CFG_IMPORT_LAYOUT_PARENT_PATH) ? rootpath.parent_path() : rootpath);
311         dir = f2i->convert(dir);
312     } else
313         dir = esc(f2i->convert(getLastPath(obj->getLocation())));
314 
315     if (!dir.empty()) {
316         id = content->addContainerChain(fmt::format("/Audio/Directories/{}", dir.string().c_str()));
317         add(obj, id);
318     }
319 }
320 
321 #ifdef SOPCAST
addSopCast(const std::shared_ptr<CdsObject> & obj)322 void BuiltinLayout::addSopCast(const std::shared_ptr<CdsObject>& obj)
323 {
324 #define SP_VPATH "/Online Services/SopCast"
325     bool ref_set = false;
326 
327     if (obj->getID() != INVALID_OBJECT_ID) {
328         obj->setRefID(obj->getID());
329         ref_set = true;
330     }
331 
332     auto id = content->addContainerChain(SP_VPATH "/All Channels");
333     add(obj, id, ref_set);
334     if (!ref_set) {
335         obj->setRefID(obj->getID());
336         ref_set = true;
337     }
338 
339     auto temp = obj->getAuxData(SOPCAST_AUXDATA_GROUP);
340     if (!temp.empty()) {
341         id = content->addContainerChain(fmt::format(SP_VPATH "/Groups/{}", esc(temp)));
342         add(obj, id, ref_set);
343     }
344 }
345 #endif
346 
347 #ifdef ATRAILERS
addATrailers(const std::shared_ptr<CdsObject> & obj)348 void BuiltinLayout::addATrailers(const std::shared_ptr<CdsObject>& obj)
349 {
350 #define AT_VPATH "/Online Services/Apple Trailers"
351     auto id = content->addContainerChain(AT_VPATH "/All Trailers");
352 
353     if (obj->getID() != INVALID_OBJECT_ID) {
354         obj->setRefID(obj->getID());
355         add(obj, id);
356     } else {
357         add(obj, id);
358         obj->setRefID(obj->getID());
359     }
360 
361     auto meta = obj->getMetaData();
362 
363     std::string temp = getValueOrDefault(meta, MetadataHandler::getMetaFieldName(M_GENRE));
364     auto genreAr = splitString(temp, ',');
365     for (auto&& genre : genreAr) {
366         trimStringInPlace(genre);
367         genre = mapGenre(genre);
368         if (genre.empty())
369             continue;
370 
371         id = content->addContainerChain(fmt::format(AT_VPATH "/Genres/{}", esc(genre)));
372         add(obj, id);
373     }
374 
375     temp = getValueOrDefault(meta, MetadataHandler::getMetaFieldName(M_DATE));
376     if (temp.length() >= 7) {
377         id = content->addContainerChain(fmt::format(AT_VPATH "/Release Date/{}", esc(temp.substr(0, 7))));
378         add(obj, id);
379     }
380 
381     temp = obj->getAuxData(ATRAILERS_AUXDATA_POST_DATE);
382     if (temp.length() >= 7) {
383         id = content->addContainerChain(fmt::format(AT_VPATH "/Post Date/{}", esc(temp.substr(0, 7))));
384         add(obj, id);
385     }
386 }
387 #endif
388 
BuiltinLayout(std::shared_ptr<ContentManager> content)389 BuiltinLayout::BuiltinLayout(std::shared_ptr<ContentManager> content)
390     : Layout(std::move(content))
391     , genreMap(config->getDictionaryOption(CFG_IMPORT_SCRIPTING_IMPORT_GENRE_MAP))
392 {
393 #ifdef ENABLE_PROFILING
394     PROF_INIT_GLOBAL(layout_profiling, "builtin layout");
395 #endif
396 }
397 
mapGenre(const std::string & genre)398 std::string BuiltinLayout::mapGenre(const std::string& genre)
399 {
400     for (auto&& [from, to] : genreMap) {
401         if (std::regex_match(genre, std::regex(from, std::regex::ECMAScript | std::regex::icase))) {
402             return std::regex_replace(genre, std::regex(from, std::regex::ECMAScript | std::regex::icase), to);
403         }
404     }
405     return genre;
406 }
407 
processCdsObject(const std::shared_ptr<CdsObject> & obj,const fs::path & rootpath,const std::string & mimetype,const std::string & content_type)408 void BuiltinLayout::processCdsObject(const std::shared_ptr<CdsObject>& obj, const fs::path& rootpath, const std::string& mimetype, const std::string& content_type)
409 {
410     log_debug("Process CDS Object: {}", obj->getTitle().c_str());
411 #ifdef ENABLE_PROFILING
412     PROF_START(&layout_profiling);
413 #endif
414     auto clone = CdsObject::createObject(obj->getObjectType());
415     obj->copyTo(clone);
416     clone->setVirtual(true);
417 
418 #ifdef ONLINE_SERVICES
419     if (clone->getFlag(OBJECT_FLAG_ONLINE_SERVICE)) {
420         auto service = service_type_t(std::stoi(clone->getAuxData(ONLINE_SERVICE_AUX_ID)));
421 
422         switch (service) {
423 #ifdef SOPCAST
424         case OS_SopCast:
425             addSopCast(clone);
426             break;
427 #endif
428 #ifdef ATRAILERS
429         case OS_ATrailers:
430             addATrailers(clone);
431             break;
432 #endif
433         case OS_Max:
434         default:
435             log_warning("No handler for service type");
436             break;
437         }
438     } else {
439 #endif
440 
441         if (startswith(mimetype, "video"))
442             addVideo(clone, rootpath);
443         else if (startswith(mimetype, "image"))
444             addImage(clone, rootpath);
445         else if ((startswith(mimetype, "audio") && (content_type != CONTENT_TYPE_PLAYLIST)))
446             addAudio(clone, rootpath);
447         else if (content_type == CONTENT_TYPE_OGG) {
448             if (obj->getFlag(OBJECT_FLAG_OGG_THEORA))
449                 addVideo(clone, rootpath);
450             else
451                 addAudio(clone, rootpath);
452         }
453 
454 #ifdef ONLINE_SERVICES
455     }
456 #endif
457 #ifdef ENABLE_PROFILING
458     PROF_END(&layout_profiling);
459 #endif
460 }
461 
462 #ifdef ENABLE_PROFILING
~BuiltinLayout()463 BuiltinLayout::~BuiltinLayout()
464 {
465     PROF_PRINT(&layout_profiling);
466 }
467 #endif
468