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