MythTV master
playlist.cpp
Go to the documentation of this file.
1// C++
2#include <algorithm>
3#include <cinttypes>
4#include <cstdlib>
5#include <map>
6#include <unistd.h>
7
8// qt
9#include <QApplication>
10#include <QFileInfo>
11#include <QObject>
12#include <QRegularExpression>
13
14// MythTV
15#include <libmythbase/compat.h>
18#include <libmythbase/mythdb.h>
23
24// mythmusic
25#include "musicdata.h"
26#include "musicplayer.h"
27#include "playlist.h"
28#include "playlistcontainer.h"
29#include "smartplaylist.h"
30
32// Playlist
33
34#define LOC QString("Playlist: ")
35#define LOC_WARN QString("Playlist, Warning: ")
36#define LOC_ERR QString("Playlist, Error: ")
37
39{
40 return m_songs.contains(trackID);
41}
42
43void Playlist::copyTracks(Playlist *to_ptr, bool update_display)
44{
46
47 for (int x = 0; x < m_songs.size(); x++)
48 {
49 MusicMetadata *mdata = getRawSongAt(x);
50 if (mdata)
51 {
52 if (mdata->isDBTrack())
53 to_ptr->addTrack(mdata->ID(), update_display);
54 }
55 }
56
58
59 changed();
60}
61
63void Playlist::addTrack(MusicMetadata::IdType trackID, bool update_display)
64{
65 int repo = ID_TO_REPO(trackID);
66 MusicMetadata *mdata = nullptr;
67
68 if (repo == RT_Radio)
69 mdata = gMusicData->m_all_streams->getMetadata(trackID);
70 else
71 mdata = gMusicData->m_all_music->getMetadata(trackID);
72
73 if (mdata)
74 {
75 m_songs.push_back(trackID);
76 m_shuffledSongs.push_back(trackID);
77
78 changed();
79
80 if (update_display && isActivePlaylist())
81 gPlayer->activePlaylistChanged(trackID, false);
82 }
83 else
84 {
85 LOG(VB_GENERAL, LOG_ERR, LOC + "Can't add track, given a bad track ID");
86 }
87}
88
90{
91 m_songs.clear();
92 m_shuffledSongs.clear();
93
94 changed();
95}
96
98{
99 // find the cd tracks
100 SongList cdTracks;
101 for (int x = 0; x < m_songs.count(); x++)
102 {
103 MusicMetadata *mdata = getRawSongAt(x);
104
105 if (mdata && mdata->isCDTrack())
106 cdTracks.append(m_songs.at(x));
107 }
108
109 // remove the tracks from our lists
110 for (int x = 0; x < cdTracks.count(); x++)
111 {
112 m_songs.removeAll(cdTracks.at(x));
113 m_shuffledSongs.removeAll(cdTracks.at(x));;
114 }
115
116 changed();
117}
118
120{
121 m_songs.removeAll(trackID);
122 m_shuffledSongs.removeAll(trackID);
123
124 changed();
125
126 if (isActivePlaylist())
127 gPlayer->activePlaylistChanged(trackID, true);
128}
129
130void Playlist::moveTrackUpDown(bool flag, int where_its_at)
131{
132 uint insertion_point = 0;
133 MusicMetadata::IdType id = m_shuffledSongs.at(where_its_at);
134
135 if (flag)
136 insertion_point = ((uint)where_its_at) - 1;
137 else
138 insertion_point = ((uint)where_its_at) + 1;
139
140 m_shuffledSongs.removeAt(where_its_at);
141 m_shuffledSongs.insert(insertion_point, id);
142
143 changed();
144}
145
147 m_name(tr("oops"))
148{
149}
150
152{
153 m_songs.clear();
154 m_shuffledSongs.clear();
155}
156
158{
159 m_shuffledSongs.clear();
160
161 switch (shuffleMode)
162 {
164 {
165 QMultiMap<int, MusicMetadata::IdType> songMap;
166
167 for (int x = 0; x < m_songs.count(); x++)
168 {
169 // Pseudo-random is good enough. Don't need a true random.
170 // NOLINTNEXTLINE(cert-msc30-c,cert-msc50-cpp)
171 songMap.insert(rand(), m_songs.at(x));
172 }
173
174 QMultiMap<int, MusicMetadata::IdType>::const_iterator i = songMap.constBegin();
175 while (i != songMap.constEnd())
176 {
177 m_shuffledSongs.append(i.value());
178 ++i;
179 }
180
181 break;
182 }
183
185 {
186 int RatingWeight = 2;
187 int PlayCountWeight = 2;
188 int LastPlayWeight = 2;
189 int RandomWeight = 2;
190 m_parent->FillIntelliWeights(RatingWeight, PlayCountWeight,
191 LastPlayWeight, RandomWeight);
192
193 // compute max/min playcount,lastplay for this playlist
194 int playcountMin = 0;
195 int playcountMax = 0;
196 double lastplayMin = 0.0;
197 double lastplayMax = 0.0;
198
199 for (int x = 0; x < m_songs.count(); x++)
200 {
201 MusicMetadata *mdata = getRawSongAt(x);
202 if (!mdata)
203 continue;
204
205 if (!mdata->isCDTrack())
206 {
207
208 if (0 == x)
209 {
210 // first song
211 playcountMin = playcountMax = mdata->PlayCount();
212 lastplayMin = lastplayMax = mdata->LastPlay().toSecsSinceEpoch();
213 }
214 else
215 {
216 if (mdata->PlayCount() < playcountMin)
217 playcountMin = mdata->PlayCount();
218 else if (mdata->PlayCount() > playcountMax)
219 playcountMax = mdata->PlayCount();
220
221 double lastplaysecs = mdata->LastPlay().toSecsSinceEpoch();
222 if (lastplaysecs < lastplayMin)
223 lastplayMin = lastplaysecs;
224 else if (lastplaysecs > lastplayMax)
225 lastplayMax = lastplaysecs;
226 }
227 }
228 }
229
230 // next we compute all the weights
231 std::map<int,double> weights;
232 std::map<int,int> ratings;
233 std::map<int,int> ratingCounts;
234 int TotalWeight = RatingWeight + PlayCountWeight + LastPlayWeight;
235 for (int x = 0; x < m_songs.size(); x++)
236 {
237 MusicMetadata *mdata = getRawSongAt(x);
238 if (mdata && !mdata->isCDTrack())
239 {
240 int rating = mdata->Rating();
241 int playcount = mdata->PlayCount();
242 double lastplaydbl = mdata->LastPlay().toSecsSinceEpoch();
243 double ratingValue = (double)(rating) / 10;
244 double playcountValue = __builtin_nan("");
245 double lastplayValue = __builtin_nan("");
246
247 if (playcountMax == playcountMin)
248 playcountValue = 0;
249 else
250 playcountValue = (((playcountMin - (double)playcount) / (playcountMax - playcountMin)) + 1);
251
252 if (lastplayMax == lastplayMin)
253 lastplayValue = 0;
254 else
255 lastplayValue = (((lastplayMin - lastplaydbl) / (lastplayMax - lastplayMin)) + 1);
256
257 double weight = (RatingWeight * ratingValue +
258 PlayCountWeight * playcountValue +
259 LastPlayWeight * lastplayValue) / TotalWeight;
260 weights[mdata->ID()] = weight;
261 ratings[mdata->ID()] = rating;
262 ++ratingCounts[rating];
263 }
264 }
265
266 // then we divide weights with the number of songs in the rating class
267 // (more songs in a class ==> lower weight, without affecting other classes)
268 double totalWeights = 0;
269 auto weightsEnd = weights.end();
270 for (auto weightsIt = weights.begin() ; weightsIt != weightsEnd ; ++weightsIt)
271 {
272 weightsIt->second /= ratingCounts[ratings[weightsIt->first]];
273 totalWeights += weightsIt->second;
274 }
275
276 // then we get a random order, balanced with relative weights of remaining songs
277 std::map<int,uint32_t> order;
278 uint32_t orderCpt = 1;
279 while (!weights.empty())
280 {
281 // Pseudo-random is good enough. Don't need a true random.
282 // NOLINTNEXTLINE(cert-msc30-c,cert-msc50-cpp)
283 double hit = totalWeights * (double)rand() / (double)RAND_MAX;
284 auto weightEnd = weights.end();
285 auto weightIt = weights.begin();
286 double pos = 0;
287 while (weightIt != weightEnd)
288 {
289 pos += weightIt->second;
290 if (pos >= hit)
291 break;
292 ++weightIt;
293 }
294
295 // FIXME If we don't exit here then we'll segfault, but it
296 // probably won't give us the desired randomisation
297 // either - There seems to be a flaw in this code, we
298 // erase items from the map but never adjust
299 // 'totalWeights' so at a point 'pos' will never be
300 // greater or equal to 'hit' and we will always hit the
301 // end of the map
302 if (weightIt == weightEnd)
303 break;
304
305 order[weightIt->first] = orderCpt;
306 totalWeights -= weightIt->second;
307 weights.erase(weightIt);
308 ++orderCpt;
309 }
310
311 // create a map of tracks sorted by the computed order
312 QMultiMap<int, MusicMetadata::IdType> songMap;
313 for (int x = 0; x < m_songs.count(); x++)
314 songMap.insert(order[m_songs.at(x)], m_songs.at(x));
315
316 // copy the shuffled tracks to the shuffled song list
317 QMultiMap<int, MusicMetadata::IdType>::const_iterator i = songMap.constBegin();
318 while (i != songMap.constEnd())
319 {
320 m_shuffledSongs.append(i.value());
321 ++i;
322 }
323
324 break;
325 }
326
328 {
329 // "intellegent/album" order
330
331 using AlbumMap = std::map<QString, uint32_t>;
332 AlbumMap album_map;
333 AlbumMap::iterator Ialbum;
334 QString album;
335
336 // pre-fill the album-map with the album name.
337 // This allows us to do album mode in album order
338 for (int x = 0; x < m_songs.count(); x++)
339 {
340 MusicMetadata *mdata = getRawSongAt(x);
341 if (mdata)
342 {
343 album = mdata->Album() + " ~ " + QString("%1").arg(mdata->getAlbumId());
344 Ialbum = album_map.find(album);
345 if (Ialbum == album_map.end())
346 album_map.insert(AlbumMap::value_type(album, 0));
347 }
348 }
349
350 // populate the sort id into the album map
351 uint32_t album_count = 1;
352 for (Ialbum = album_map.begin(); Ialbum != album_map.end(); ++Ialbum)
353 {
354 Ialbum->second = album_count;
355 album_count++;
356 }
357
358 // create a map of tracks sorted by the computed order
359 QMultiMap<int, MusicMetadata::IdType> songMap;
360 for (int x = 0; x < m_songs.count(); x++)
361 {
362 MusicMetadata *mdata = getRawSongAt(x);
363 if (mdata)
364 {
365 uint32_t album_order = 1;
366 album = album = mdata->Album() + " ~ " + QString("%1").arg(mdata->getAlbumId());;
367 Ialbum = album_map.find(album);
368 if (Ialbum == album_map.end())
369 {
370 // we didn't find this album in the map,
371 // yet we pre-loaded them all. we are broken,
372 // but we just set the track order to 1, since there
373 // is no real point in reporting an error
374 album_order = 1;
375 }
376 else
377 {
378 album_order = Ialbum->second * 10000;
379 }
380 if (mdata->DiscNumber() != -1)
381 album_order += mdata->DiscNumber()*100;
382 album_order += mdata->Track();
383
384 songMap.insert(album_order, m_songs.at(x));
385 }
386 }
387
388 // copy the shuffled tracks to the shuffled song list
389 QMultiMap<int, MusicMetadata::IdType>::const_iterator i = songMap.constBegin();
390 while (i != songMap.constEnd())
391 {
392 m_shuffledSongs.append(i.value());
393 ++i;
394 }
395
396 break;
397 }
398
400 {
401 // "intellegent/album" order
402
403 using ArtistMap = std::map<QString, uint32_t>;
404 ArtistMap artist_map;
405 ArtistMap::iterator Iartist;
406 QString artist;
407
408 // pre-fill the album-map with the album name.
409 // This allows us to do artist mode in artist order
410 for (int x = 0; x < m_songs.count(); x++)
411 {
412 MusicMetadata *mdata = getRawSongAt(x);
413 if (mdata)
414 {
415 artist = mdata->Artist() + " ~ " + mdata->Title();
416 Iartist = artist_map.find(artist);
417 if (Iartist == artist_map.end())
418 artist_map.insert(ArtistMap::value_type(artist,0));
419 }
420 }
421
422 // populate the sort id into the artist map
423 uint32_t artist_count = 1;
424 for (Iartist = artist_map.begin(); Iartist != artist_map.end(); ++Iartist)
425 {
426 Iartist->second = artist_count;
427 artist_count++;
428 }
429
430 // create a map of tracks sorted by the computed order
431 QMultiMap<int, MusicMetadata::IdType> songMap;
432 for (int x = 0; x < m_songs.count(); x++)
433 {
434 MusicMetadata *mdata = getRawSongAt(x);
435 if (mdata)
436 {
437 uint32_t artist_order = 1;
438 artist = mdata->Artist() + " ~ " + mdata->Title();
439 Iartist = artist_map.find(artist);
440 if (Iartist == artist_map.end())
441 {
442 // we didn't find this artist in the map,
443 // yet we pre-loaded them all. we are broken,
444 // but we just set the track order to 1, since there
445 // is no real point in reporting an error
446 artist_order = 1;
447 }
448 else
449 {
450 artist_order = Iartist->second * 1000;
451 }
452 artist_order += mdata->Track();
453
454 songMap.insert(artist_order, m_songs.at(x));
455 }
456 }
457
458 // copy the shuffled tracks to the shuffled song list
459 QMultiMap<int, MusicMetadata::IdType>::const_iterator i = songMap.constBegin();
460 while (i != songMap.constEnd())
461 {
462 m_shuffledSongs.append(i.value());
463 ++i;
464 }
465
466 break;
467 }
468
469 default:
470 {
471 // copy the raw song list to the shuffled track list
472 // NOLINTNEXTLINE(modernize-loop-convert)
473 for (auto it = m_songs.begin(); it != m_songs.end(); ++it)
474 m_shuffledSongs.append(*it);
475
476 break;
477 }
478 }
479}
480
482{
483 // This is for debugging
484#if 0
485 LOG(VB_GENERAL, LOG_DEBUG,
486 QString("Playlist with name of \"%1\"").arg(name));
487 LOG(VB_GENERAL, LOG_DEBUG,
488 QString(" playlistid is %1").arg(laylistid));
489 LOG(VB_GENERAL, LOG_DEBUG,
490 QString(" songlist(raw) is \"%1\"").arg(raw_songlist));
491 LOG(VB_GENERAL, LOG_DEBUG, " songlist list is ");
492#endif
493
494 QString msg;
495 for (int x = 0; x < m_songs.count(); x++)
496 msg += QString("%1,").arg(m_songs.at(x));
497
498 LOG(VB_GENERAL, LOG_INFO, LOC + msg);
499}
500
501void Playlist::getStats(uint *trackCount, std::chrono::seconds *totalLength,
502 uint currenttrack, std::chrono::seconds *playedLength) const
503{
504 std::chrono::milliseconds total = 0ms;
505 std::chrono::milliseconds played = 0ms;
506
507 *trackCount = m_shuffledSongs.size();
508
509 if ((int)currenttrack >= m_shuffledSongs.size())
510 currenttrack = 0;
511
512 for (int x = 0; x < m_shuffledSongs.count(); x++)
513 {
514 MusicMetadata *mdata = getSongAt(x);
515 if (mdata)
516 {
517 total += mdata->Length();
518 if (x < (int)currenttrack)
519 played += mdata->Length();
520 }
521 }
522
523 if (playedLength)
524 *playedLength = duration_cast<std::chrono::seconds>(played);
525
526 *totalLength = duration_cast<std::chrono::seconds>(total);
527}
528
529void Playlist::loadPlaylist(const QString& a_name, const QString& a_host)
530{
531 QString rawSonglist;
532
533 if (a_host.isEmpty())
534 {
535 LOG(VB_GENERAL, LOG_ERR, LOC +
536 "loadPlaylist() - We need a valid hostname");
537 return;
538 }
539
541
542 if (m_name == "default_playlist_storage" ||
543 m_name == "stream_playlist")
544 {
545 query.prepare("SELECT playlist_id, playlist_name, playlist_songs "
546 "FROM music_playlists "
547 "WHERE playlist_name = :NAME"
548 " AND hostname = :HOST;");
549 }
550 else
551 {
552 // Technically this is never called as this function
553 // is only used to load the default playlist.
554 query.prepare("SELECT playlist_id, playlist_name, playlist_songs "
555 "FROM music_playlists "
556 "WHERE playlist_name = :NAME"
557 " AND (hostname = '' OR hostname = :HOST);");
558 }
559 query.bindValue(":NAME", a_name);
560 query.bindValue(":HOST", a_host);
561
562 if (query.exec() && query.size() > 0)
563 {
564 while (query.next())
565 {
566 m_playlistid = query.value(0).toInt();
567 m_name = query.value(1).toString();
568 rawSonglist = query.value(2).toString();
569 }
570 }
571 else
572 {
573 // Asked me to load a playlist I can't find so let's create a new one :)
574 m_playlistid = 0; // Be safe just in case we call load over the top
575 // of an existing playlist
576 rawSonglist.clear();
577 savePlaylist(a_name, a_host);
578 }
579
580 fillSongsFromSonglist(rawSonglist);
581
583}
584
585void Playlist::loadPlaylistByID(int id, const QString& a_host)
586{
587 QString rawSonglist;
589 query.prepare("SELECT playlist_id, playlist_name, playlist_songs "
590 "FROM music_playlists "
591 "WHERE playlist_id = :ID"
592 " AND (hostname = '' OR hostname = :HOST);");
593 query.bindValue(":ID", id);
594 query.bindValue(":HOST", a_host);
595
596 if (!query.exec())
597 MythDB::DBError("Playlist::loadPlaylistByID", query);
598
599 while (query.next())
600 {
601 m_playlistid = query.value(0).toInt();
602 m_name = query.value(1).toString();
603 rawSonglist = query.value(2).toString();
604 }
605
606 if (m_name == "default_playlist_storage")
607 m_name = tr("Default Playlist");
608
609 fillSongsFromSonglist(rawSonglist);
610}
611
614{
615 bool needUpdate = false;
616
617 for (int x = 0; x < m_songs.count(); x++)
618 {
620 MusicMetadata *mdata = getRawSongAt(x);
621 if (!mdata)
622 {
623 m_songs.removeAll(id);
624 m_shuffledSongs.removeAll(id);
625 needUpdate = true;
626 }
627 }
628
629 if (needUpdate)
630 {
631 changed();
632
634
635 // TODO check we actually need this
636 if (isActivePlaylist())
637 gPlayer->activePlaylistChanged(-1, false);
638 }
639}
640
641void Playlist::fillSongsFromSonglist(const QString& songList)
642{
643 bool badTrack = false;
644
645 QStringList list = songList.split(",", Qt::SkipEmptyParts);
646 for (const auto & song : std::as_const(list))
647 {
648 MusicMetadata::IdType id = song.toUInt();
649 int repo = ID_TO_REPO(id);
650 if (repo == RT_Radio)
651 {
652 // check this is a valid stream ID
654 m_songs.push_back(id);
655 else
656 {
657 badTrack = true;
658 LOG(VB_GENERAL, LOG_ERR, LOC + QString("Got a bad track %1").arg(id));
659 }
660 }
661 else
662 {
663 // check this is a valid track ID
665 m_songs.push_back(id);
666 else
667 {
668 badTrack = true;
669 LOG(VB_GENERAL, LOG_ERR, LOC + QString("Got a bad track %1").arg(id));
670 }
671 }
672 }
673
674 if (this == gPlayer->getCurrentPlaylist())
676 else
678
679 if (badTrack)
680 changed();
681
682 if (isActivePlaylist())
683 gPlayer->activePlaylistChanged(-1, false);
684}
685
686int Playlist::fillSonglistFromQuery(const QString& whereClause,
687 bool removeDuplicates,
688 InsertPLOption insertOption,
689 int currentTrackID)
690{
691 QString orig_songlist = toRawSonglist();
692 QString new_songlist;
693 int added = 0;
694
695 disableSaves();
697
699
700 QString theQuery;
701
702 theQuery = "SELECT song_id FROM music_songs "
703 "LEFT JOIN music_directories ON"
704 " music_songs.directory_id=music_directories.directory_id "
705 "LEFT JOIN music_artists ON"
706 " music_songs.artist_id=music_artists.artist_id "
707 "LEFT JOIN music_albums ON"
708 " music_songs.album_id=music_albums.album_id "
709 "LEFT JOIN music_genres ON"
710 " music_songs.genre_id=music_genres.genre_id "
711 "LEFT JOIN music_artists AS music_comp_artists ON "
712 "music_albums.artist_id=music_comp_artists.artist_id ";
713 if (whereClause.length() > 0)
714 theQuery += whereClause;
715
716 if (!query.exec(theQuery))
717 {
718 MythDB::DBError("Load songlist from query", query);
719 new_songlist.clear();
720 fillSongsFromSonglist(new_songlist);
721 enableSaves();
722 changed();
723 return 0;
724 }
725
726 while (query.next())
727 {
728 new_songlist += "," + query.value(0).toString();
729 added++;
730 }
731 new_songlist.remove(0, 1);
732
733 if (removeDuplicates && insertOption != PL_REPLACE)
734 orig_songlist = removeItemsFromList(new_songlist, orig_songlist);
735
736 switch (insertOption)
737 {
738 case PL_REPLACE:
739 break;
740
742 new_songlist = new_songlist + "," + orig_songlist;
743 break;
744
745 case PL_INSERTATEND:
746 new_songlist = orig_songlist + "," + new_songlist;
747 break;
748
750 {
751 QStringList list = orig_songlist.split(",", Qt::SkipEmptyParts);
752 bool bFound = false;
753 QString tempList;
754 for (const auto& song : std::as_const(list))
755 {
756 int an_int = song.toInt();
757 tempList += "," + song;
758 if (!bFound && an_int == currentTrackID)
759 {
760 bFound = true;
761 tempList += "," + new_songlist;
762 }
763 }
764
765 if (!bFound)
766 tempList = orig_songlist + "," + new_songlist;
767
768 new_songlist = tempList.remove(0, 1);
769
770 break;
771 }
772
773 default:
774 new_songlist = orig_songlist;
775 }
776
777 fillSongsFromSonglist(new_songlist);
778
779 enableSaves();
780 changed();
781 return added;
782}
783
784// songList is a list of trackIDs to add
785int Playlist::fillSonglistFromList(const QList<int> &songList,
786 bool removeDuplicates,
787 InsertPLOption insertOption,
788 int currentTrackID)
789{
790 QString orig_songlist = toRawSonglist();
791 QString new_songlist;
792
793 disableSaves();
794
796
797 for (int x = 0; x < songList.count(); x++)
798 {
799 new_songlist += "," + QString::number(songList.at(x));
800 }
801 new_songlist.remove(0, 1);
802
803 if (removeDuplicates && insertOption != PL_REPLACE)
804 orig_songlist = removeItemsFromList(new_songlist, orig_songlist);
805
806 switch (insertOption)
807 {
808 case PL_REPLACE:
809 break;
810
812 new_songlist = new_songlist + "," + orig_songlist;
813 break;
814
815 case PL_INSERTATEND:
816 new_songlist = orig_songlist + "," + new_songlist;
817 break;
818
820 {
821 QStringList list = orig_songlist.split(",", Qt::SkipEmptyParts);
822 bool bFound = false;
823 QString tempList;
824 for (const auto & song : std::as_const(list))
825 {
826 int an_int = song.toInt();
827 tempList += "," + song;
828 if (!bFound && an_int == currentTrackID)
829 {
830 bFound = true;
831 tempList += "," + new_songlist;
832 }
833 }
834
835 if (!bFound)
836 tempList = orig_songlist + "," + new_songlist;
837
838 new_songlist = tempList.remove(0, 1);
839
840 break;
841 }
842
843 default:
844 new_songlist = orig_songlist;
845 }
846
847 fillSongsFromSonglist(new_songlist);
848
849 enableSaves();
850
851 changed();
852 return songList.count();
853}
854
855QString Playlist::toRawSonglist(bool shuffled, bool tracksOnly)
856{
857 QString rawList = "";
858
859 if (shuffled)
860 {
861 for (int x = 0; x < m_shuffledSongs.count(); x++)
862 {
864 if (tracksOnly)
865 {
866 if (ID_TO_REPO(id) == RT_Database)
867 rawList += QString(",%1").arg(id);
868 }
869 else
870 {
871 rawList += QString(",%1").arg(id);
872 }
873 }
874 }
875 else
876 {
877 for (int x = 0; x < m_songs.count(); x++)
878 {
880 if (tracksOnly)
881 {
882 if (ID_TO_REPO(id) == RT_Database)
883 rawList += QString(",%1").arg(id);
884 }
885 else
886 {
887 rawList += QString(",%1").arg(id);
888 }
889 }
890 }
891
892 if (!rawList.isEmpty())
893 rawList = rawList.remove(0, 1);
894
895 return rawList;
896}
897
898int Playlist::fillSonglistFromSmartPlaylist(const QString& category, const QString& name,
899 bool removeDuplicates,
900 InsertPLOption insertOption,
901 int currentTrackID)
902{
904
905 // find the correct categoryid
906 int categoryID = SmartPlaylistEditor::lookupCategoryID(category);
907 if (categoryID == -1)
908 {
909 LOG(VB_GENERAL, LOG_WARNING, LOC +
910 QString("Cannot find Smartplaylist Category: %1") .arg(category));
911 return 0;
912 }
913
914 // find smartplaylist
915 int ID = 0;
916 QString matchType;
917 QString orderBy;
918 int limitTo = 0;
919
920 query.prepare("SELECT smartplaylistid, matchtype, orderby, limitto "
921 "FROM music_smartplaylists "
922 "WHERE categoryid = :CATEGORYID AND name = :NAME;");
923 query.bindValue(":NAME", name);
924 query.bindValue(":CATEGORYID", categoryID);
925
926 if (query.exec())
927 {
928 if (query.isActive() && query.size() > 0)
929 {
930 query.first();
931 ID = query.value(0).toInt();
932 matchType = (query.value(1).toString() == "All") ? " AND " : " OR ";
933 orderBy = query.value(2).toString();
934 limitTo = query.value(3).toInt();
935 }
936 else
937 {
938 LOG(VB_GENERAL, LOG_WARNING, LOC +
939 QString("Cannot find smartplaylist: %1").arg(name));
940 return 0;
941 }
942 }
943 else
944 {
945 MythDB::DBError("Find SmartPlaylist", query);
946 return 0;
947 }
948
949 // get smartplaylist items
950 QString whereClause = "WHERE ";
951
952 query.prepare("SELECT field, operator, value1, value2 "
953 "FROM music_smartplaylist_items "
954 "WHERE smartplaylistid = :ID;");
955 query.bindValue(":ID", ID);
956 if (query.exec())
957 {
958 bool bFirst = true;
959 while (query.next())
960 {
961 QString fieldName = query.value(0).toString();
962 QString operatorName = query.value(1).toString();
963 QString value1 = query.value(2).toString();
964 QString value2 = query.value(3).toString();
965 if (!bFirst)
966 {
967 whereClause += matchType + getCriteriaSQL(fieldName,
968 operatorName, value1, value2);
969 }
970 else
971 {
972 bFirst = false;
973 whereClause += " " + getCriteriaSQL(fieldName, operatorName,
974 value1, value2);
975 }
976 }
977 }
978
979 // add order by clause
980 whereClause += getOrderBySQL(orderBy);
981
982 // add limit
983 if (limitTo > 0)
984 whereClause += " LIMIT " + QString::number(limitTo);
985
986 return fillSonglistFromQuery(whereClause, removeDuplicates,
987 insertOption, currentTrackID);
988}
989
991{
992 m_changed = true;
993
994 if (m_doSave)
996}
997
998void Playlist::savePlaylist(const QString& a_name, const QString& a_host)
999{
1000 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Saving playlist: " + a_name);
1001
1002 m_name = a_name.simplified();
1003 if (m_name.isEmpty())
1004 {
1005 LOG(VB_GENERAL, LOG_WARNING, LOC + "Not saving unnamed playlist");
1006 return;
1007 }
1008
1009 if (a_host.isEmpty())
1010 {
1011 LOG(VB_GENERAL, LOG_WARNING, LOC +
1012 "Not saving playlist without a host name");
1013 return;
1014 }
1015
1016 // get the shuffled list of tracks excluding any cd tracks and radio streams
1017 QString rawSonglist = toRawSonglist(true, true);
1018
1020 uint songcount = 0;
1021 std::chrono::seconds playtime = 0s;
1022
1023 getStats(&songcount, &playtime);
1024
1025 bool save_host = ("default_playlist_storage" == a_name);
1026 if (m_playlistid > 0)
1027 {
1028 QString str_query = "UPDATE music_playlists SET "
1029 "playlist_songs = :LIST, "
1030 "playlist_name = :NAME, "
1031 "songcount = :SONGCOUNT, "
1032 "length = :PLAYTIME";
1033 if (save_host)
1034 str_query += ", hostname = :HOSTNAME";
1035 str_query += " WHERE playlist_id = :ID ;";
1036
1037 query.prepare(str_query);
1038 query.bindValue(":ID", m_playlistid);
1039 }
1040 else
1041 {
1042 QString str_query = "INSERT INTO music_playlists"
1043 " (playlist_name, playlist_songs,"
1044 " songcount, length";
1045 if (save_host)
1046 str_query += ", hostname";
1047 str_query += ") VALUES(:NAME, :LIST, :SONGCOUNT, :PLAYTIME";
1048 if (save_host)
1049 str_query += ", :HOSTNAME";
1050 str_query += ");";
1051
1052 query.prepare(str_query);
1053 }
1054 query.bindValue(":LIST", rawSonglist);
1055 query.bindValue(":NAME", a_name);
1056 query.bindValue(":SONGCOUNT", songcount);
1057 query.bindValue(":PLAYTIME", qlonglong(playtime.count()));
1058 if (save_host)
1059 query.bindValue(":HOSTNAME", a_host);
1060
1061 if (!query.exec() || (m_playlistid < 1 && query.numRowsAffected() < 1))
1062 {
1063 MythDB::DBError("Problem saving playlist", query);
1064 }
1065
1066 if (m_playlistid < 1)
1067 m_playlistid = query.lastInsertId().toInt();
1068
1069 m_changed = false;
1070}
1071
1072// Return a copy of the second list, having removed any item that
1073// also appears in the first list.
1074//
1075// @param remove_list A comma separated list of strings to be
1076// removed from the source list.
1077// @param source_list A comma separated list of strings to be
1078// processed.
1079// @return A comma separated list of the strings remaining after
1080// processing.
1081QString Playlist::removeItemsFromList(const QString &remove_list, const QString &source_list)
1082{
1083 QStringList removeList = remove_list.split(",", Qt::SkipEmptyParts);
1084 QStringList sourceList = source_list.split(",", Qt::SkipEmptyParts);
1085 QString songlist;
1086
1087 for (const auto & song : std::as_const(sourceList))
1088 {
1089 if (removeList.indexOf(song) == -1)
1090 songlist += "," + song;
1091 }
1092 songlist.remove(0, 1);
1093 return songlist;
1094}
1095
1097{
1098 MusicMetadata *mdata = nullptr;
1099
1100 if (pos >= 0 && pos < m_shuffledSongs.size())
1101 {
1103 int repo = ID_TO_REPO(id);
1104
1105 if (repo == RT_Radio)
1106 mdata = gMusicData->m_all_streams->getMetadata(id);
1107 else
1108 mdata = gMusicData->m_all_music->getMetadata(id);
1109 }
1110
1111 return mdata;
1112}
1113
1115{
1116 MusicMetadata *mdata = nullptr;
1117
1118 if (pos >= 0 && pos < m_songs.size())
1119 {
1120 MusicMetadata::IdType id = m_songs.at(pos);
1121 int repo = ID_TO_REPO(id);
1122
1123 if (repo == RT_Radio)
1124 mdata = gMusicData->m_all_streams->getMetadata(id);
1125 else
1126 mdata = gMusicData->m_all_music->getMetadata(id);
1127 }
1128
1129 return mdata;
1130}
1131
1132// Here begins CD Writing things. ComputeSize, CreateCDMP3 & CreateCDAudio
1133// FIXME none of this is currently used
1134#ifdef CD_WRTITING_FIXED
1135void Playlist::computeSize(double &size_in_MB, double &size_in_sec)
1136{
1137 //double child_MB;
1138 //double child_sec;
1139
1140 // Clear return values
1141 size_in_MB = 0.0;
1142 size_in_sec = 0.0;
1143
1144 for (int x = 0; x < m_songs.size(); x++)
1145 {
1146 MusicMetadata *mdata = getRawSongAt(x);
1147 if (mdata)
1148 {
1149 if (mdata->isCDTrack())
1150 continue;
1151
1152 // Normal track
1153 if (mdata->Length() > 0ms)
1154 size_in_sec += duration_cast<floatsecs>(mdata->Length()).count();
1155 else
1156 LOG(VB_GENERAL, LOG_ERR, "Computing track lengths. "
1157 "One track <=0");
1158
1159 size_in_MB += mdata->FileSize() / 1000000;
1160 }
1161 }
1162}
1163
1164void Playlist::cdrecordData(int fd)
1165{
1166 if (!m_progress || !m_proc)
1167 return;
1168
1169 QByteArray buf;
1170 if (fd == 1)
1171 {
1172 buf = m_proc->ReadAll();
1173
1174 // I would just use the QTextStream::readLine(), but wodim uses \r
1175 // to update the same line, so I'm splitting it on \r or \n
1176 // Track 01: 6 of 147 MB written (fifo 100%) [buf 99%] 16.3x.
1177 QString data(buf);
1178 static const QRegularExpression newline { "\\R" }; // Any unicode newline
1179 QStringList list = data.split(newline, Qt::SkipEmptyParts);
1180
1181 for (int i = 0; i < list.size(); i++)
1182 {
1183 QString line = list.at(i);
1184
1185 if (line.mid(15, 2) == "of")
1186 {
1187 int mbdone = line.mid(10, 5).trimmed().toInt();
1188 int mbtotal = line.mid(17, 5).trimmed().toInt();
1189
1190 if (mbtotal > 0)
1191 {
1192 m_progress->setProgress((mbdone * 100) / mbtotal);
1193 }
1194 }
1195 }
1196 }
1197 else
1198 {
1199 buf = m_proc->ReadAllErr();
1200
1201 QTextStream text(buf);
1202
1203 while (!text.atEnd())
1204 {
1205 QString err = text.readLine();
1206 if (err.contains("Drive needs to reload the media") ||
1207 err.contains("Input/output error.") ||
1208 err.contains("No disk / Wrong disk!"))
1209 {
1210 LOG(VB_GENERAL, LOG_ERR, err);
1211 m_proc->Term();
1212 }
1213 }
1214 }
1215}
1216
1217void Playlist::mkisofsData(int fd)
1218{
1219 if (!m_progress || !m_proc)
1220 return;
1221
1222 QByteArray buf;
1223 if (fd == 1)
1224 buf = m_proc->ReadAll();
1225 else
1226 {
1227 buf = m_proc->ReadAllErr();
1228
1229 QTextStream text(buf);
1230
1231 while (!text.atEnd())
1232 {
1233 QString line = text.readLine();
1234 if (line[6] == '%')
1235 {
1236 line = line.mid(0, 3);
1237 m_progress->setProgress(line.trimmed().toInt());
1238 }
1239 }
1240 }
1241}
1242
1243void Playlist::processExit(uint retval)
1244{
1245 m_procExitVal = retval;
1246}
1247
1248void Playlist::processExit(void)
1249{
1250 m_procExitVal = GENERIC_EXIT_OK;
1251}
1252
1253// FIXME: this needs updating to work with storage groups
1254int Playlist::CreateCDMP3(void)
1255{
1256 // Check & get global settings
1257 if (!gCoreContext->GetNumSetting("CDWriterEnabled"))
1258 {
1259 LOG(VB_GENERAL, LOG_ERR, "CD Writer is not enabled.");
1260 return 1;
1261 }
1262
1263 QString scsidev = MediaMonitor::defaultCDWriter();
1264 if (scsidev.isEmpty())
1265 {
1266 LOG(VB_GENERAL, LOG_ERR, "No CD Writer device defined.");
1267 return 1;
1268 }
1269
1270 int disksize = gCoreContext->GetNumSetting("CDDiskSize", 2);
1271 QString writespeed = gCoreContext->GetSetting("CDWriteSpeed", "2");
1272 bool MP3_dir_flag = gCoreContext->GetNumSetting("CDCreateDir", 1);
1273
1274 double size_in_MB = 0.0;
1275
1276 QStringList reclist;
1277
1278 for (int x = 0; x < m_shuffledSongs.count(); x++)
1279 {
1280 MusicMetadata *mdata = getRawSongAt(x);
1281
1282 // Normal track
1283 if (mdata)
1284 {
1285 if (mdata->isCDTrack())
1286 continue;
1287
1288 // check filename..
1289 QFileInfo testit(mdata->Filename());
1290 if (!testit.exists())
1291 continue;
1292 size_in_MB += testit.size() / 1000000.0;
1293 QString outline;
1294 if (MP3_dir_flag)
1295 {
1296 if (mdata->Artist().length() > 0)
1297 outline += mdata->Artist() + "/";
1298 if (mdata->Album().length() > 0)
1299 outline += mdata->Album() + "/";
1300 }
1301
1302 outline += "=";
1303 outline += mdata->Filename();
1304
1305 reclist += outline;
1306 }
1307 }
1308
1309 int max_size;
1310 if (disksize == 0)
1311 max_size = 650;
1312 else
1313 max_size = 700;
1314
1315 if (size_in_MB >= max_size)
1316 {
1317 LOG(VB_GENERAL, LOG_ERR, "MP3 CD creation aborted -- cd size too big.");
1318 return 1;
1319 }
1320
1321 // probably should tie stdout of mkisofs to stdin of cdrecord sometime
1322 QString tmptemplate("/tmp/mythmusicXXXXXX");
1323
1324 QString tmprecordlist = createTempFile(tmptemplate);
1325 if (tmprecordlist == tmptemplate)
1326 {
1327 LOG(VB_GENERAL, LOG_ERR, "Unable to open temporary file");
1328 return 1;
1329 }
1330
1331 QString tmprecordisofs = createTempFile(tmptemplate);
1332 if (tmprecordisofs == tmptemplate)
1333 {
1334 LOG(VB_GENERAL, LOG_ERR, "Unable to open temporary file");
1335 return 1;
1336 }
1337
1338 QFile reclistfile(tmprecordlist);
1339
1340 if (!reclistfile.open(QIODevice::WriteOnly))
1341 {
1342 LOG(VB_GENERAL, LOG_ERR, "Unable to open temporary file");
1343 return 1;
1344 }
1345
1346 QTextStream recstream(&reclistfile);
1347
1348 QStringList::Iterator iter;
1349
1350 for (iter = reclist.begin(); iter != reclist.end(); ++iter)
1351 {
1352 recstream << *iter << "\n";
1353 }
1354
1355 reclistfile.close();
1356
1357 m_progress = new MythProgressDialog(tr("Creating CD File System"),
1358 100);
1359 m_progress->setProgress(1);
1360
1361 QStringList args;
1362 QString command;
1363
1364 command = "mkisofs";
1365 args << "-graft-points";
1366 args << "-path-list";
1367 args << tmprecordlist;
1368 args << "-o";
1369 args << tmprecordisofs;
1370 args << "-J";
1371 args << "-R";
1372
1373 uint flags = kMSRunShell | kMSStdErr |
1376
1377 m_proc = new MythSystemLegacy(command, args, flags);
1378
1379 connect(m_proc, &MythSystemLegacy::readDataReady, this, &Playlist::mkisofsData,
1380 Qt::DirectConnection);
1381 connect(m_proc, &MythSystemLegacy::finished, this, qOverload<>(&Playlist::processExit),
1382 Qt::DirectConnection);
1383 connect(m_proc, &MythSystemLegacy::error, this, qOverload<uint>(&Playlist::processExit),
1384 Qt::DirectConnection);
1385
1386 m_procExitVal = GENERIC_EXIT_RUNNING;
1387 m_proc->Run();
1388
1389 while( m_procExitVal == GENERIC_EXIT_RUNNING )
1390 usleep( 100000 );
1391
1392 uint retval = m_procExitVal;
1393
1394 m_progress->Close();
1395 m_progress->deleteLater();
1396 m_proc->disconnect();
1397 delete m_proc;
1398
1399 if (retval)
1400 {
1401 LOG(VB_GENERAL, LOG_ERR, QString("Unable to run mkisofs: returns %1")
1402 .arg(retval));
1403 }
1404 else
1405 {
1406 m_progress = new MythProgressDialog(tr("Burning CD"), 100);
1407 m_progress->setProgress(2);
1408
1409 command = "cdrecord";
1410 args = QStringList();
1411 args << "-v";
1412 //args << "-dummy";
1413 args << QString("dev=%1").arg(scsidev);
1414
1415 if (writespeed.toInt() > 0)
1416 {
1417 args << "-speed=";
1418 args << writespeed;
1419 }
1420
1421 args << "-data";
1422 args << tmprecordisofs;
1423
1424 flags = kMSRunShell | kMSStdErr | kMSStdOut |
1427
1428 m_proc = new MythSystemLegacy(command, args, flags);
1429 connect(m_proc, &MythSystemLegacy::readDataReady,
1430 this, &Playlist::cdrecordData, Qt::DirectConnection);
1431 connect(m_proc, &MythSystemLegacy::finished,
1432 this, qOverload<>(&Playlist::processExit), Qt::DirectConnection);
1433 connect(m_proc, &MythSystemLegacy::error,
1434 this, qOverload<uint>(&Playlist::processExit), Qt::DirectConnection);
1435 m_procExitVal = GENERIC_EXIT_RUNNING;
1436 m_proc->Run();
1437
1438 while( m_procExitVal == GENERIC_EXIT_RUNNING )
1439 usleep( 100000 );
1440
1441 retval = m_procExitVal;
1442
1443 m_progress->Close();
1444 m_progress->deleteLater();
1445 m_proc->disconnect();
1446 delete m_proc;
1447
1448 if (retval)
1449 {
1450 LOG(VB_GENERAL, LOG_ERR,
1451 QString("Unable to run cdrecord: returns %1") .arg(retval));
1452 }
1453 }
1454
1455 QFile::remove(tmprecordlist);
1456 QFile::remove(tmprecordisofs);
1457
1458 return retval;
1459}
1460
1461int Playlist::CreateCDAudio(void)
1462{
1463 return -1;
1464}
1465#endif
MusicMetadata * getMetadata(int an_id)
bool isValidID(int an_id)
bool isValidID(MusicMetadata::IdType an_id)
MusicMetadata * getMetadata(MusicMetadata::IdType an_id)
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:837
bool first(void)
Wrap QSqlQuery::first() so we can display the query results.
Definition: mythdbcon.cpp:822
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
int numRowsAffected() const
Definition: mythdbcon.h:217
bool isActive(void) const
Definition: mythdbcon.h:215
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:618
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:888
QVariant lastInsertId()
Return the id of the last inserted row.
Definition: mythdbcon.cpp:935
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:812
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:550
static QString defaultCDWriter()
CDWriterDeviceLocation, user-selected drive, or /dev/cdrom.
AllMusic * m_all_music
Definition: musicdata.h:52
AllStream * m_all_streams
Definition: musicdata.h:53
bool isCDTrack(void) const
bool isDBTrack(void) const
QDateTime LastPlay() const
std::chrono::milliseconds Length() const
QString Title() const
int Track() const
IdType ID() const
QString Filename(bool find=true)
QString Artist() const
int DiscNumber() const
int Rating() const
uint64_t FileSize() const
int PlayCount() const
uint32_t IdType
Definition: musicmetadata.h:86
QString Album() const
void playlistChanged(int playlistID)
@ SHUFFLE_INTELLIGENT
Definition: musicplayer.h:177
void activePlaylistChanged(int trackID, bool deleted)
ShuffleMode getShuffleMode(void)
Definition: musicplayer.h:195
Playlist * getCurrentPlaylist(void)
QString GetHostName(void)
QString GetSetting(const QString &key, const QString &defaultval="")
int GetNumSetting(const QString &key, int defaultval=0)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:226
void error(uint status)
void finished(void)
void readDataReady(int fd)
void FillIntelliWeights(int &rating, int &playcount, int &lastplay, int &random) const
void loadPlaylistByID(int id, const QString &a_host)
Definition: playlist.cpp:585
void removeAllCDTracks(void)
Definition: playlist.cpp:97
MusicMetadata * getRawSongAt(int pos) const
Definition: playlist.cpp:1114
QString toRawSonglist(bool shuffled=false, bool tracksOnly=false)
Definition: playlist.cpp:855
bool m_doSave
Definition: playlist.h:143
void copyTracks(Playlist *to_ptr, bool update_display)
Definition: playlist.cpp:43
void resync(void)
make sure all tracks are still valid after a scan
Definition: playlist.cpp:613
bool isActivePlaylist(void)
Definition: playlist.h:109
int fillSonglistFromSmartPlaylist(const QString &category, const QString &name, bool removeDuplicates=false, InsertPLOption insertOption=PL_REPLACE, int currentTrackID=0)
Definition: playlist.cpp:898
static QString removeItemsFromList(const QString &remove_list, const QString &source_list)
Definition: playlist.cpp:1081
void enableSaves(void)
Definition: playlist.h:103
void describeYourself(void) const
Definition: playlist.cpp:481
void removeAllTracks(void)
Definition: playlist.cpp:89
int fillSonglistFromQuery(const QString &whereClause, bool removeDuplicates=false, InsertPLOption insertOption=PL_REPLACE, int currentTrackID=0)
Definition: playlist.cpp:686
void changed(void)
Definition: playlist.cpp:990
void fillSongsFromSonglist(const QString &songList)
Definition: playlist.cpp:641
PlaylistContainer * m_parent
Definition: playlist.h:141
void disableSaves(void)
whether any changes should be saved to the DB
Definition: playlist.h:102
SongList m_shuffledSongs
Definition: playlist.h:139
void savePlaylist(const QString &a_name, const QString &a_host)
Definition: playlist.cpp:998
void getStats(uint *trackCount, std::chrono::seconds *totalLength, uint currentTrack=0, std::chrono::seconds *playedLength=nullptr) const
Definition: playlist.cpp:501
void moveTrackUpDown(bool flag, int where_its_at)
Definition: playlist.cpp:130
void loadPlaylist(const QString &a_name, const QString &a_host)
Definition: playlist.cpp:529
~Playlist() override
Definition: playlist.cpp:151
QString m_name
Definition: playlist.h:136
void shuffleTracks(MusicPlayer::ShuffleMode mode)
Definition: playlist.cpp:157
MusicMetadata * getSongAt(int pos) const
Definition: playlist.cpp:1096
bool m_changed
Definition: playlist.h:142
Playlist(void)
Definition: playlist.cpp:146
SongList m_songs
Definition: playlist.h:138
int m_playlistid
Definition: playlist.h:135
int fillSonglistFromList(const QList< int > &songList, bool removeDuplicates, InsertPLOption insertOption, int currentTrackID)
Definition: playlist.cpp:785
void removeTrack(MusicMetadata::IdType trackID)
Definition: playlist.cpp:119
void addTrack(MusicMetadata::IdType trackID, bool update_display)
Given a tracks ID, add that track to this playlist.
Definition: playlist.cpp:63
bool checkTrack(MusicMetadata::IdType trackID) const
Definition: playlist.cpp:38
static int lookupCategoryID(const QString &category)
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
@ GENERIC_EXIT_RUNNING
Process is running.
Definition: exitcodes.h:28
unsigned int uint
Definition: freesurround.h:24
MusicData * gMusicData
Definition: musicdata.cpp:23
@ RT_Radio
Definition: musicmetadata.h:62
@ RT_Database
Definition: musicmetadata.h:60
static constexpr uint32_t ID_TO_REPO(uint32_t x)
Definition: musicmetadata.h:71
MusicPlayer * gPlayer
Definition: musicplayer.cpp:38
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QString createTempFile(QString name_template, bool dir)
@ kMSDontBlockInputDevs
avoid blocking LIRC & Joystick Menu
Definition: mythsystem.h:36
@ kMSStdErr
allow access to stderr
Definition: mythsystem.h:42
@ kMSStdOut
allow access to stdout
Definition: mythsystem.h:41
@ kMSRunShell
run process through shell
Definition: mythsystem.h:43
@ kMSRunBackground
run child in the background
Definition: mythsystem.h:38
@ kMSDontDisableDrawing
avoid disabling UI drawing
Definition: mythsystem.h:37
def rating(profile, smoonURL, gate)
Definition: scan.py:36
#define LOC
Definition: playlist.cpp:34
QList< MusicMetadata::IdType > SongList
Definition: playlist.h:43
InsertPLOption
Definition: playlist.h:23
@ PL_INSERTAFTERCURRENT
Definition: playlist.h:27
@ PL_INSERTATBEGINNING
Definition: playlist.h:25
@ PL_REPLACE
Definition: playlist.h:24
@ PL_INSERTATEND
Definition: playlist.h:26
QString getOrderBySQL(const QString &orderByFields)
QString getCriteriaSQL(const QString &fieldName, const QString &operatorName, QString value1, QString value2)