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