MythTV  0.27pre
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Groups Pages
cdrip.cpp
Go to the documentation of this file.
1 // ANSI C includes
2 #include <cstdio>
3 #include <cstring>
4 
5 // Unix C includes
6 #include <sys/types.h>
7 #include <fcntl.h>
8 
9 #include "config.h"
10 #ifdef HAVE_CDIO
11 # include <cdio/cdda.h>
12 # include <cdio/paranoia.h>
13 #endif //def HAVE_CDIO
14 
15 // C++ includes
16 #include <iostream>
17 #include <memory>
18 using namespace std;
19 
20 // Qt includes
21 #include <QApplication>
22 #include <QDir>
23 #include <QRegExp>
24 #include <QKeyEvent>
25 #include <QEvent>
26 #include <QFile>
27 
28 // MythTV plugin includes
29 #include <mythcontext.h>
30 #include <mythdb.h>
31 #include <lcddevice.h>
32 #include <mythmediamonitor.h>
33 
34 // MythUI
35 #include <mythdialogbox.h>
36 #include <mythuitext.h>
37 #include <mythuicheckbox.h>
38 #include <mythuitextedit.h>
39 #include <mythuibutton.h>
40 #include <mythuiprogressbar.h>
41 #include <mythuibuttonlist.h>
42 #include <mythsystem.h>
43 
44 // MythUI headers
45 #include <mythtv/libmythui/mythscreenstack.h>
46 #include <mythtv/libmythui/mythprogressdialog.h>
47 
48 // MythMusic includes
49 #include "cdrip.h"
50 #ifdef HAVE_CDIO
51 #include "cddecoder.h"
52 #endif // HAVE_CDIO
53 #include "encoder.h"
54 #include "vorbisencoder.h"
55 #include "lameencoder.h"
56 #include "flacencoder.h"
57 #include "genres.h"
58 #include "editmetadata.h"
59 #include "mythlogging.h"
60 #include "musicutils.h"
61 
62 #ifdef HAVE_CDIO
63 // libparanoia compatibility
64 #ifndef cdrom_paranoia
65 #define cdrom_paranoia cdrom_paranoia_t
66 #endif // cdrom_paranoia
67 
68 #ifndef CD_FRAMESIZE_RAW
69 # define CD_FRAMESIZE_RAW CDIO_CD_FRAMESIZE_RAW
70 #endif // CD_FRAMESIZE_RAW
71 #endif // HAVE_CDIO
72 
74  (QEvent::Type) QEvent::registerEventType();
76  (QEvent::Type) QEvent::registerEventType();
78  (QEvent::Type) QEvent::registerEventType();
80  (QEvent::Type) QEvent::registerEventType();
82  (QEvent::Type) QEvent::registerEventType();
84  (QEvent::Type) QEvent::registerEventType();
86  (QEvent::Type) QEvent::registerEventType();
88  (QEvent::Type) QEvent::registerEventType();
90  (QEvent::Type) QEvent::registerEventType();
91 QEvent::Type RipStatusEvent::kFinishedEvent =
92  (QEvent::Type) QEvent::registerEventType();
94  (QEvent::Type) QEvent::registerEventType();
95 
97  MThread("CDScanner"), m_parent(ripper)
98 {
99 }
100 
102 {
103  RunProlog();
104  m_parent->scanCD();
105  RunEpilog();
106 }
107 
109 
111  MThread("CDEjector"), m_parent(ripper)
112 {
113 }
114 
116 {
117  RunProlog();
118  m_parent->ejectCD();
119  RunEpilog();
120 }
121 
123 
124 static long int getSectorCount (QString &cddevice, int tracknum)
125 {
126 #ifdef HAVE_CDIO
127  QByteArray devname = cddevice.toAscii();
128  cdrom_drive *device = cdda_identify(devname.constData(), 0, NULL);
129 
130  if (!device)
131  {
132  LOG(VB_GENERAL, LOG_ERR,
133  QString("Error: %1('%2',track=%3) failed at cdda_identify()").
134  arg(__func__).arg(cddevice).arg(tracknum));
135  return -1;
136  }
137 
138  if (cdda_open(device))
139  {
140  LOG(VB_GENERAL, LOG_ERR,
141  QString("Error: %1('%2',track=%3) failed at cdda_open() - cdda not supported").
142  arg(__func__).arg(cddevice).arg(tracknum));
143  cdda_close(device);
144  return -1;
145  }
146 
147  // we only care about audio tracks
148  if (cdda_track_audiop (device, tracknum))
149  {
150  cdda_verbose_set(device, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
151  long int start = cdda_track_firstsector(device, tracknum);
152  long int end = cdda_track_lastsector( device, tracknum);
153  cdda_close(device);
154  return end - start + 1;
155  }
156  LOG(VB_GENERAL, LOG_ERR,
157  QString("Error: cdrip - cdda_track_audiop(%1) returned 0").arg(cddevice));
158 
159  cdda_close(device);
160 #else
161  (void)cddevice; (void)tracknum;
162 #endif // HAVE_CDIO
163  return 0;
164 }
165 
166 #ifdef HAVE_CDIO
167 static void paranoia_cb(long, paranoia_cb_mode_t)
168 {
169 }
170 #endif // HAVE_CDIO
171 
173  QVector<RipTrack*> *tracks, int quality) :
174  MThread("CDRipper"),
175  m_parent(parent), m_quit(false),
176  m_CDdevice(device), m_quality(quality),
177  m_tracks(tracks), m_totalSectors(0),
178  m_totalSectorsDone(0), m_lastTrackPct(0),
179  m_lastOverallPct(0)
180 {
181 #ifdef WIN32 // libcdio needs the drive letter with no path
182  if (m_CDdevice.endsWith('\\'))
183  m_CDdevice.chop(1);
184 #endif // WIN32
185 }
186 
188 {
189  cancel();
190  wait();
191 }
192 
194 {
195  m_quit = true;
196 }
197 
199 {
200  return m_quit;
201 }
202 
204 {
205  RunProlog();
206  if (!m_tracks->size() > 0)
207  {
208  RunEpilog();
209  return;
210  }
211 
212  MusicMetadata *track = m_tracks->at(0)->metadata;
213  QString tots;
214 
215  if (track->Compilation())
216  {
217  tots = track->CompilationArtist() + " ~ " + track->Album();
218  }
219  else
220  {
221  tots = track->Artist() + " ~ " + track->Album();
222  }
223 
224  QApplication::postEvent(
225  m_parent,
227  QApplication::postEvent(
228  m_parent,
230  QApplication::postEvent(
231  m_parent,
233 
234  QString textstatus;
235  QString encodertype = gCoreContext->GetSetting("EncoderType");
236  bool mp3usevbr = gCoreContext->GetNumSetting("Mp3UseVBR", 0);
237 
238  m_totalSectors = 0;
239  m_totalSectorsDone = 0;
240  for (int trackno = 0; trackno < m_tracks->size(); trackno++)
241  {
242  m_totalSectors += getSectorCount(m_CDdevice, trackno + 1);
243  }
244 
245  QApplication::postEvent(m_parent,
247 
248  if (LCD *lcd = LCD::Get())
249  {
250  QString lcd_tots = QObject::tr("Importing ") + tots;
251  QList<LCDTextItem> textItems;
252  textItems.append(LCDTextItem(1, ALIGN_CENTERED,
253  lcd_tots, "Generic", false));
254  lcd->switchToGeneric(textItems);
255  }
256 
257  MusicMetadata *titleTrack = NULL;
258  QString outfile;
259 
260  std::auto_ptr<Encoder> encoder;
261 
262  for (int trackno = 0; trackno < m_tracks->size(); trackno++)
263  {
264  if (isCancelled())
265  break;
266 
267  QApplication::postEvent(
268  m_parent,
270  QString("Track %1 of %2")
271  .arg(trackno + 1).arg(m_tracks->size())));
272 
273  QApplication::postEvent(
274  m_parent,
276 
277  track = m_tracks->at(trackno)->metadata;
278 
279  if (track)
280  {
281  textstatus = track->Title();
282  QApplication::postEvent(
283  m_parent,
284  new RipStatusEvent(
285  RipStatusEvent::kTrackTextEvent, textstatus));
286  QApplication::postEvent(
287  m_parent,
289  QApplication::postEvent(
290  m_parent,
292 
293  // do we need to start a new file?
294  if (m_tracks->at(trackno)->active)
295  {
296  titleTrack = track;
297  titleTrack->setLength(m_tracks->at(trackno)->length);
298 
299  outfile = filenameFromMetadata(track);
300 
301  if (m_quality < 3)
302  {
303  if (encodertype == "mp3")
304  {
305  outfile += ".mp3";
306  encoder.reset(new LameEncoder(getMusicDirectory() + outfile, m_quality,
307  titleTrack, mp3usevbr));
308  }
309  else // ogg
310  {
311  outfile += ".ogg";
312  encoder.reset(new VorbisEncoder(getMusicDirectory() + outfile, m_quality,
313  titleTrack));
314  }
315  }
316  else
317  {
318  outfile += ".flac";
319  encoder.reset(new FlacEncoder(getMusicDirectory() + outfile, m_quality,
320  titleTrack));
321  }
322 
323  if (!encoder->isValid())
324  {
325  QApplication::postEvent(
326  m_parent,
327  new RipStatusEvent(
329  "Encoder failed to open file for writing"));
330  LOG(VB_GENERAL, LOG_ERR, "MythMusic: Encoder failed"
331  " to open file for writing");
332 
333  RunEpilog();
334  return;
335  }
336  }
337 
338  if (!encoder.get())
339  {
340  // This should never happen.
341  QApplication::postEvent(
342  m_parent,
344  "Failed to create encoder"));
345  LOG(VB_GENERAL, LOG_ERR, "MythMusic: No encoder, failing");
346  RunEpilog();
347  return;
348  }
349  ripTrack(m_CDdevice, encoder.get(), trackno + 1);
350 
351  if (isCancelled())
352  {
353  RunEpilog();
354  return;
355  }
356 
357  // save the metadata to the DB
358  if (m_tracks->at(trackno)->active)
359  {
360  titleTrack->setFilename(outfile);
361  titleTrack->setFileSize((quint64)QFileInfo(outfile).size());
362  titleTrack->dumpToDatabase();
363  }
364  }
365  }
366 
367  QString PostRipCDScript = gCoreContext->GetSetting("PostCDRipScript");
368 
369  if (!PostRipCDScript.isEmpty())
370  myth_system(PostRipCDScript);
371 
372  QApplication::postEvent(
374 
375  RunEpilog();
376 }
377 
378 int CDRipperThread::ripTrack(QString &cddevice, Encoder *encoder, int tracknum)
379 {
380 #ifdef HAVE_CDIO
381  QByteArray devname = cddevice.toAscii();
382  cdrom_drive *device = cdda_identify(devname.constData(), 0, NULL);
383 
384  if (!device)
385  {
386  LOG(VB_GENERAL, LOG_ERR,
387  QString("cdda_identify failed for device '%1', "
388  "CDRipperThread::ripTrack(tracknum = %2) exiting.")
389  .arg(cddevice).arg(tracknum));
390  return -1;
391  }
392 
393  if (cdda_open(device))
394  {
395  LOG(VB_MEDIA, LOG_INFO,
396  QString("Error: %1('%2',track=%3) failed at cdda_open() - cdda not supported")
397  .arg(__func__).arg(cddevice).arg(tracknum));
398  cdda_close(device);
399  return -1;
400  }
401 
402  cdda_verbose_set(device, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
403  long int start = cdda_track_firstsector(device, tracknum);
404  long int end = cdda_track_lastsector(device, tracknum);
405  LOG(VB_MEDIA, LOG_INFO, QString("%1(%2,track=%3) start=%4 end=%5")
406  .arg(__func__).arg(cddevice).arg(tracknum).arg(start).arg(end));
407 
408  cdrom_paranoia *paranoia = paranoia_init(device);
409  if (gCoreContext->GetSetting("ParanoiaLevel") == "full")
410  paranoia_modeset(paranoia, PARANOIA_MODE_FULL |
411  PARANOIA_MODE_NEVERSKIP);
412  else
413  paranoia_modeset(paranoia, PARANOIA_MODE_OVERLAP);
414 
415  paranoia_seek(paranoia, start, SEEK_SET);
416 
417  long int curpos = start;
418  int16_t *buffer;
419 
420  QApplication::postEvent(
421  m_parent,
422  new RipStatusEvent(RipStatusEvent::kTrackStartEvent, end - start + 1));
423  m_lastTrackPct = -1;
424  m_lastOverallPct = -1;
425 
426  int every15 = 15;
427  while (curpos < end)
428  {
429  buffer = paranoia_read(paranoia, paranoia_cb);
430 
431  if (encoder->addSamples(buffer, CD_FRAMESIZE_RAW))
432  break;
433 
434  curpos++;
435 
436  every15--;
437 
438  if (every15 <= 0)
439  {
440  every15 = 15;
441 
442  // updating the UITypes can be slow - only update if we need to:
443  int newOverallPct = (int) (100.0 / (double) ((double) m_totalSectors /
444  (double) (m_totalSectorsDone + curpos - start)));
445  if (newOverallPct != m_lastOverallPct)
446  {
447  m_lastOverallPct = newOverallPct;
448  QApplication::postEvent(
449  m_parent,
451  newOverallPct));
452  QApplication::postEvent(
453  m_parent,
455  m_totalSectorsDone + curpos - start));
456  }
457 
458  int newTrackPct = (int) (100.0 / (double) ((double) (end - start + 1) /
459  (double) (curpos - start)));
460  if (newTrackPct != m_lastTrackPct)
461  {
462  m_lastTrackPct = newTrackPct;
463  QApplication::postEvent(
464  m_parent,
466  newTrackPct));
467  QApplication::postEvent(
468  m_parent,
470  curpos - start));
471  }
472 
473  if (LCD *lcd = LCD::Get())
474  {
475  float fProgress = (float)(m_totalSectorsDone + (curpos - start))
476  / m_totalSectors;
477  lcd->setGenericProgress(fProgress);
478  }
479  }
480 
481  if (isCancelled())
482  {
483  break;
484  }
485  }
486 
487  m_totalSectorsDone += end - start + 1;
488 
489  paranoia_free(paranoia);
490  cdda_close(device);
491 
492  return (curpos - start + 1) * CD_FRAMESIZE_RAW;
493 #else
494  (void)cddevice; (void)encoder; (void)tracknum;
495  return 0;
496 #endif // HAVE_CDIO
497 }
498 
500 
501 Ripper::Ripper(MythScreenStack *parent, QString device) :
502  MythScreenType(parent, "ripcd"),
503  m_decoder(NULL),
504 
505  m_artistEdit(NULL),
506  m_albumEdit(NULL),
507  m_genreEdit(NULL),
508  m_yearEdit(NULL),
509 
510  m_compilationCheck(NULL),
511 
512  m_trackList(NULL),
513  m_qualityList(NULL),
514 
515  m_switchTitleArtist(NULL),
516  m_scanButton(NULL),
517  m_ripButton(NULL),
518  m_searchArtistButton(NULL),
519  m_searchAlbumButton(NULL),
520  m_searchGenreButton(NULL),
521 
522  m_tracks(new QVector<RipTrack*>),
523 
524  m_somethingwasripped(false),
525  m_mediaMonitorActive(false),
526 
527  m_CDdevice(device),
528 
529  m_ejectThread(NULL), m_scanThread(NULL)
530 {
531 #ifndef _WIN32
532  // if the MediaMonitor is running stop it
533  m_mediaMonitorActive = false;
535  if (mon && mon->IsActive())
536  {
537  m_mediaMonitorActive = true;
538  mon->StopMonitoring();
539  }
540 #endif // _WIN32
541 }
542 
544 {
545  if (m_decoder)
546  delete m_decoder;
547 
548 #ifndef _WIN32
549  // if the MediaMonitor was active when we started then restart it
551  {
553  if (mon)
554  mon->StartMonitoring();
555  }
556 #endif // _WIN32
557 
559  emit ripFinished();
560 }
561 
562 bool Ripper::Create(void)
563 {
564  if (!LoadWindowFromXML("music-ui.xml", "cdripper", this))
565  return false;
566 
567  m_qualityList = dynamic_cast<MythUIButtonList *>(GetChild("quality"));
568  m_artistEdit = dynamic_cast<MythUITextEdit *>(GetChild("artist"));
569  m_searchArtistButton = dynamic_cast<MythUIButton *>(GetChild("searchartist"));
570  m_albumEdit = dynamic_cast<MythUITextEdit *>(GetChild("album"));
571  m_searchAlbumButton = dynamic_cast<MythUIButton *>(GetChild("searchalbum"));
572  m_genreEdit = dynamic_cast<MythUITextEdit *>(GetChild("genre"));
573  m_yearEdit = dynamic_cast<MythUITextEdit *>(GetChild("year"));
574  m_searchGenreButton = dynamic_cast<MythUIButton *>(GetChild("searchgenre"));
575  m_compilationCheck = dynamic_cast<MythUICheckBox *>(GetChild("compilation"));
576  m_switchTitleArtist = dynamic_cast<MythUIButton *>(GetChild("switch"));
577  m_scanButton = dynamic_cast<MythUIButton *>(GetChild("scan"));
578  m_ripButton = dynamic_cast<MythUIButton *>(GetChild("rip"));
579  m_trackList = dynamic_cast<MythUIButtonList *>(GetChild("tracks"));
580 
581  BuildFocusList();
582 
587  {
588  LOG(VB_GENERAL, LOG_ERR,
589  "Missing theme elements for screen 'cdripper'");
590  return false;
591  }
592 
593  connect(m_trackList, SIGNAL(itemClicked(MythUIButtonListItem *)),
595  connect(m_ripButton, SIGNAL(Clicked()), SLOT(startRipper()));
596  connect(m_scanButton, SIGNAL(Clicked()), SLOT(startScanCD()));
597  connect(m_switchTitleArtist, SIGNAL(Clicked()),
598  SLOT(switchTitlesAndArtists()));
599  connect(m_compilationCheck, SIGNAL(toggled(bool)),
600  SLOT(compilationChanged(bool)));
601  connect(m_searchGenreButton, SIGNAL(Clicked()), SLOT(searchGenre()));
602  connect(m_genreEdit, SIGNAL(valueChanged()), SLOT(genreChanged()));
605  connect(m_yearEdit, SIGNAL(valueChanged()), SLOT(yearChanged()));
606  connect(m_artistEdit, SIGNAL(valueChanged()), SLOT(artistChanged()));
607  connect(m_searchArtistButton, SIGNAL(Clicked()), SLOT(searchArtist()));
608  connect(m_albumEdit, SIGNAL(valueChanged()), SLOT(albumChanged()));
609  connect(m_searchAlbumButton, SIGNAL(Clicked()), SLOT(searchAlbum()));
610 
611  // Populate Quality List
612  new MythUIButtonListItem(m_qualityList, tr("Low"), qVariantFromValue(0));
613  new MythUIButtonListItem(m_qualityList, tr("Medium"), qVariantFromValue(1));
614  new MythUIButtonListItem(m_qualityList, tr("High"), qVariantFromValue(2));
615  new MythUIButtonListItem(m_qualityList, tr("Perfect"), qVariantFromValue(3));
616  m_qualityList->SetValueByData(qVariantFromValue(
617  gCoreContext->GetNumSetting("DefaultRipQuality", 1)));
618 
619  QTimer::singleShot(500, this, SLOT(startScanCD()));
620 
621  return true;
622 }
623 
624 bool Ripper::keyPressEvent(QKeyEvent *event)
625 {
626  if (GetFocusWidget() && GetFocusWidget()->keyPressEvent(event))
627  return true;
628 
629  bool handled = false;
630  QStringList actions;
631  handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions);
632 
633  for (int i = 0; i < actions.size() && !handled; i++)
634  {
635  QString action = actions[i];
636  handled = true;
637 
638  if (action == "EDIT" || action == "INFO") // INFO purely for historical reasons
639  {
641  }
642  else
643  handled = false;
644  }
645 
646  if (!handled && MythScreenType::keyPressEvent(event))
647  handled = true;
648 
649  return handled;
650 }
651 
653 {
654  if (m_scanThread)
655  return;
656 
657  QString message = QObject::tr("Scanning CD. Please Wait ...");
658  OpenBusyPopup(message);
659 
660  m_scanThread = new CDScannerThread(this);
661  connect(m_scanThread->qthread(), SIGNAL(finished()), SLOT(ScanFinished()));
662  m_scanThread->start();
663 }
664 
666 {
667  delete m_scanThread;
668  m_scanThread = NULL;
669 
670  m_tracks->clear();
671 
672  bool isCompilation = false;
673  if (m_decoder)
674  {
675  QString label;
676  MusicMetadata *metadata;
677 
678  m_artistName.clear();
679  m_albumName.clear();
680  m_genreName.clear();
681  m_year.clear();
682 
683  for (int trackno = 0; trackno < m_decoder->getNumTracks(); trackno++)
684  {
685  RipTrack *ripTrack = new RipTrack;
686 
687  metadata = m_decoder->getMetadata(trackno + 1);
688  if (metadata)
689  {
690  ripTrack->metadata = metadata;
691  ripTrack->length = metadata->Length();
692 
693  if (metadata->Compilation())
694  {
695  isCompilation = true;
696  m_artistName = metadata->CompilationArtist();
697  }
698  else if (m_artistName.isEmpty())
699  {
700  m_artistName = metadata->Artist();
701  }
702 
703  if (m_albumName.isEmpty())
704  m_albumName = metadata->Album();
705 
706  if (m_genreName.isEmpty() && !metadata->Genre().isEmpty())
707  m_genreName = metadata->Genre();
708 
709  if (m_year.isEmpty() && metadata->Year() > 0)
710  m_year = QString::number(metadata->Year());
711 
712  QString title = metadata->Title();
713  ripTrack->isNew = isNewTune(m_artistName, m_albumName, title);
714 
715  ripTrack->active = ripTrack->isNew;
716 
717  m_tracks->push_back(ripTrack);
718 
719  }
720  else
721  delete ripTrack;
722  }
723 
728  m_compilationCheck->SetCheckState(isCompilation);
729 
730  if (!isCompilation)
732  else
734  }
735 
736  BuildFocusList();
737  updateTrackList();
738 
739  CloseBusyPopup();
740 }
741 
742 void Ripper::scanCD(void)
743 {
744 #ifdef HAVE_CDIO
745  {
746  LOG(VB_MEDIA, LOG_INFO, QString("Ripper::%1 CD='%2'").
747  arg(__func__).arg(m_CDdevice));
748  (void)cdio_close_tray(m_CDdevice.toAscii().constData(), NULL);
749  }
750 #endif // HAVE_CDIO
751 
752  if (m_decoder)
753  delete m_decoder;
754 
755  m_decoder = new CdDecoder("cda", NULL, NULL);
756  if (m_decoder)
758 }
759 
761 {
762  QVector<RipTrack*>::iterator it;
763  for (it = m_tracks->begin(); it < m_tracks->end(); ++it)
764  {
765  RipTrack *track = (*it);
766  if (track && !track->isNew)
767  {
768  if (deleteExistingTrack(track))
769  {
770  track->isNew = true;
771  toggleTrackActive(track);
772  }
773  }
774  }
775 }
776 
778 {
779  if (!track)
780  return false;
781 
782  MusicMetadata *metadata = track->metadata;
783 
784  if (!metadata)
785  return false;
786 
787  QString artist = metadata->Artist();
788  QString album = metadata->Album();
789  QString title = metadata->Title();
790 
791  MSqlQuery query(MSqlQuery::InitCon());
792  QString queryString("SELECT song_id, "
793  "CONCAT_WS('/', music_directories.path, music_songs.filename) AS filename "
794  "FROM music_songs "
795  "LEFT JOIN music_artists"
796  " ON music_songs.artist_id=music_artists.artist_id "
797  "LEFT JOIN music_albums"
798  " ON music_songs.album_id=music_albums.album_id "
799  "LEFT JOIN music_directories "
800  " ON music_songs.directory_id=music_directories.directory_id "
801  "WHERE artist_name REGEXP \'");
802  QString token = artist;
803  token.replace(QRegExp("(/|\\\\|:|\'|\\,|\\!|\\(|\\)|\"|\\?|\\|)"),
804  QString("."));
805 
806  queryString += token + "\' AND " + "album_name REGEXP \'";
807  token = album;
808  token.replace(QRegExp("(/|\\\\|:|\'|\\,|\\!|\\(|\\)|\"|\\?|\\|)"),
809  QString("."));
810  queryString += token + "\' AND " + "name REGEXP \'";
811  token = title;
812  token.replace(QRegExp("(/|\\\\|:|\'|\\,|\\!|\\(|\\)|\"|\\?|\\|)"),
813  QString("."));
814  queryString += token + "\' ORDER BY artist_name, album_name,"
815  " name, song_id, filename LIMIT 1";
816  query.prepare(queryString);
817 
818  if (!query.exec() || !query.isActive())
819  {
820  MythDB::DBError("Search music database", query);
821  return false;
822  }
823 
824  if (query.next())
825  {
826  int trackID = query.value(0).toInt();
827  QString filename = getMusicDirectory() + query.value(1).toString();
828 
829  // delete file
830  if (!QFile::remove(filename))
831  {
832  LOG(VB_GENERAL, LOG_NOTICE, QString("Ripper::deleteExistingTrack() "
833  "Could not delete %1")
834  .arg(filename));
835  return false;
836  }
837 
838  // remove database entry
839  MSqlQuery deleteQuery(MSqlQuery::InitCon());
840  deleteQuery.prepare("DELETE FROM music_songs"
841  " WHERE song_id = :SONG_ID");
842  deleteQuery.bindValue(":SONG_ID", trackID);
843  if (!deleteQuery.exec())
844  {
845  MythDB::DBError("Delete Track", deleteQuery);
846  return false;
847  }
848  return true;
849  }
850 
851  return false;
852 }
853 
855 {
856  return m_somethingwasripped;
857 }
858 
860 {
861  QString newartist = m_artistEdit->GetText();
863 
864  if (m_tracks->size() > 0)
865  {
866  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
867  {
868  data = m_tracks->at(trackno)->metadata;
869 
870  if (data)
871  {
873  {
874  data->setCompilationArtist(newartist);
875  }
876  else
877  {
878  data->setArtist(newartist);
879  data->setCompilationArtist("");
880  }
881  }
882  }
883 
884  updateTrackList();
885  }
886 
887  m_artistName = newartist;
888 }
889 
891 {
892  QString newalbum = m_albumEdit->GetText();
894 
895  if (m_tracks->size() > 0)
896  {
897  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
898  {
899  data = m_tracks->at(trackno)->metadata;
900 
901  if (data)
902  data->setAlbum(newalbum);
903  }
904  }
905 
906  m_albumName = newalbum;
907 }
908 
910 {
911  QString newgenre = m_genreEdit->GetText();
913 
914  if (m_tracks->size() > 0)
915  {
916  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
917  {
918  data = m_tracks->at(trackno)->metadata;
919 
920  if (data)
921  data->setGenre(newgenre);
922  }
923  }
924 
925  m_genreName = newgenre;
926 }
927 
929 {
930  QString newyear = m_yearEdit->GetText();
931 
933 
934  if (m_tracks->size() > 0)
935  {
936  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
937  {
938  data = m_tracks->at(trackno)->metadata;
939 
940  if (data)
941  data->setYear(newyear.toInt());
942  }
943  }
944 
945  m_year = newyear;
946 }
947 
949 {
950  if (!state)
951  {
953  if (m_tracks->size() > 0)
954  {
955  // Update artist MetaData of each track on the ablum...
956  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
957  {
958  data = m_tracks->at(trackno)->metadata;
959 
960  if (data)
961  {
962  data->setCompilationArtist("");
963  data->setArtist(m_artistName);
964  data->setCompilation(false);
965  }
966  }
967  }
968 
970  }
971  else
972  {
973  if (m_tracks->size() > 0)
974  {
975  // Update artist MetaData of each track on the album...
976  for (int trackno = 0; trackno < m_tracks->size(); ++trackno)
977  {
979  data = m_tracks->at(trackno)->metadata;
980 
981  if (data)
982  {
984  data->setCompilation(true);
985  }
986  }
987  }
988 
990  }
991 
992  BuildFocusList();
993  updateTrackList();
994 }
995 
997 {
999  return;
1000 
1002 
1003  // Switch title and artist for each track
1004  QString tmp;
1005  if (m_tracks->size() > 0)
1006  {
1007  for (int track = 0; track < m_tracks->size(); ++track)
1008  {
1009  data = m_tracks->at(track)->metadata;
1010 
1011  if (data)
1012  {
1013  tmp = data->Artist();
1014  data->setArtist(data->Title());
1015  data->setTitle(tmp);
1016  }
1017  }
1018 
1019  updateTrackList();
1020  }
1021 }
1022 
1024 {
1025  if (m_tracks->isEmpty())
1026  {
1027  ShowOkPopup(tr("There are no tracks to rip?"));
1028  return;
1029  }
1030 
1032 
1033  int quality = m_qualityList->GetItemCurrent()->GetData().toInt();
1034 
1035  RipStatus *statusDialog = new RipStatus(mainStack, m_CDdevice, m_tracks,
1036  quality);
1037 
1038  if (statusDialog->Create())
1039  {
1040  connect(statusDialog, SIGNAL(Result(bool)), SLOT(RipComplete(bool)));
1041  mainStack->AddScreen(statusDialog);
1042  }
1043  else
1044  delete statusDialog;
1045 }
1046 
1047 void Ripper::RipComplete(bool result)
1048 {
1049  if (result == true)
1050  {
1051  bool EjectCD = gCoreContext->GetNumSetting("EjectCDAfterRipping", 1);
1052  if (EjectCD)
1053  startEjectCD();
1054 
1055  ShowOkPopup(tr("Rip completed successfully."));
1056 
1057  m_somethingwasripped = true;
1058  }
1059 
1060  if (LCD *lcd = LCD::Get())
1061  lcd->switchToTime();
1062 }
1063 
1064 
1066 {
1067  if (m_ejectThread)
1068  return;
1069 
1070  QString message = tr("Ejecting CD. Please Wait ...");
1071 
1072  OpenBusyPopup(message);
1073 
1074  m_ejectThread = new CDEjectorThread(this);
1075  connect(m_ejectThread->qthread(),
1076  SIGNAL(finished()), SLOT(EjectFinished()));
1077  m_ejectThread->start();
1078 }
1079 
1081 {
1082  delete m_ejectThread;
1083  m_ejectThread = NULL;
1084 
1085  CloseBusyPopup();
1086 }
1087 
1089 {
1090  LOG(VB_MEDIA, LOG_INFO, __PRETTY_FUNCTION__);
1091  bool bEjectCD = gCoreContext->GetNumSetting("EjectCDAfterRipping",1);
1092  if (bEjectCD)
1093  {
1094 #ifdef HAVE_CDIO
1095  LOG(VB_MEDIA, LOG_INFO, QString("Ripper::%1 '%2'").
1096  arg(__func__).arg(m_CDdevice));
1097  (void)cdio_eject_media_drive(m_CDdevice.toAscii().constData());
1098 #else
1100  if (mon)
1101  {
1102  QByteArray devname = m_CDdevice.toAscii();
1103  MythMediaDevice *pMedia = mon->GetMedia(devname.constData());
1104  if (pMedia && mon->ValidateAndLock(pMedia))
1105  {
1106  pMedia->eject();
1107  mon->Unlock(pMedia);
1108  }
1109  }
1110 #endif // HAVE_CDIO
1111  }
1112 }
1113 
1115 {
1116  if (m_tracks->isEmpty())
1117  return;
1118 
1119  QString tmptitle;
1120  if (m_trackList)
1121  {
1122  m_trackList->Reset();
1123 
1124  int i;
1125  for (i = 0; i < (int)m_tracks->size(); i++)
1126  {
1127  if (i >= m_tracks->size())
1128  break;
1129 
1130  RipTrack *track = m_tracks->at(i);
1131  MusicMetadata *metadata = track->metadata;
1132 
1134 
1135  item->setCheckable(true);
1136 
1137  item->SetData(qVariantFromValue(track));
1138 
1139  if (track->isNew)
1140  item->DisplayState("new", "yes");
1141  else
1142  item->DisplayState("new", "no");
1143 
1144  if (track->active)
1146  else
1148 
1149  item->SetText(QString::number(metadata->Track()), "track");
1150  item->SetText(metadata->Title(), "title");
1151  item->SetText(metadata->Artist(), "artist");
1152 
1153  int length = track->length / 1000;
1154  if (length > 0)
1155  {
1156  int min, sec;
1157  min = length / 60;
1158  sec = length % 60;
1159  QString s;
1160  s.sprintf("%02d:%02d", min, sec);
1161  item->SetText(s, "length");
1162  }
1163  else
1164  item->SetText("", "length");
1165 
1166 // if (i == m_currentTrack)
1167 // m_trackList->SetItemCurrent(i);
1168  }
1169  }
1170 }
1171 
1173 {
1174  QString msg = tr("Select an Artist");
1175  QStringList searchList = MusicMetadata::fillFieldList("artist");
1176 
1177  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
1178  MythUISearchDialog *searchDlg = new MythUISearchDialog(popupStack, msg, searchList, false, "");
1179 
1180  if (!searchDlg->Create())
1181  {
1182  delete searchDlg;
1183  return;
1184  }
1185 
1186  connect(searchDlg, SIGNAL(haveResult(QString)), SLOT(setArtist(QString)));
1187 
1188  popupStack->AddScreen(searchDlg);
1189 }
1190 
1191 void Ripper::setArtist(QString artist)
1192 {
1193  m_artistEdit->SetText(artist);
1194 }
1195 
1197 {
1198  QString msg = tr("Select an Album");
1199  QStringList searchList = MusicMetadata::fillFieldList("album");
1200 
1201  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
1202  MythUISearchDialog *searchDlg = new MythUISearchDialog(popupStack, msg, searchList, false, "");
1203 
1204  if (!searchDlg->Create())
1205  {
1206  delete searchDlg;
1207  return;
1208  }
1209 
1210  connect(searchDlg, SIGNAL(haveResult(QString)), SLOT(setAlbum(QString)));
1211 
1212  popupStack->AddScreen(searchDlg);
1213 }
1214 
1215 void Ripper::setAlbum(QString album)
1216 {
1217  m_albumEdit->SetText(album);
1218 }
1219 
1221 {
1222  QString msg = tr("Select a Genre");
1223  QStringList searchList = MusicMetadata::fillFieldList("genre");
1224  // load genre list
1225  m_searchList.clear();
1226  for (int x = 0; x < genre_table_size; x++)
1227  m_searchList.push_back(QString(genre_table[x]));
1228  m_searchList.sort();
1229 
1230  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
1231  MythUISearchDialog *searchDlg = new MythUISearchDialog(popupStack, msg, searchList, false, "");
1232 
1233  if (!searchDlg->Create())
1234  {
1235  delete searchDlg;
1236  return;
1237  }
1238 
1239  connect(searchDlg, SIGNAL(haveResult(QString)), SLOT(setGenre(QString)));
1240 
1241  popupStack->AddScreen(searchDlg);
1242 }
1243 
1244 void Ripper::setGenre(QString genre)
1245 {
1246  m_genreEdit->SetText(genre);
1247 }
1248 
1250 {
1251  if (!item || m_tracks->isEmpty())
1252  return;
1253 
1254  RipTrack *track = qVariantValue<RipTrack *>(item->GetData());
1255 
1256  if (!track)
1257  return;
1258 
1259  MusicMetadata *editMeta = track->metadata;
1260 
1262 
1263  EditMetadataDialog *editDialog = new EditMetadataDialog(mainStack, editMeta);
1264  editDialog->setSaveMetadataOnly();
1265 
1266  if (!editDialog->Create())
1267  {
1268  delete editDialog;
1269  return;
1270  }
1271 
1272  connect(editDialog, SIGNAL(metadataChanged()), this, SLOT(metadataChanged()));
1273 
1274  mainStack->AddScreen(editDialog);
1275 }
1276 
1278 {
1279  updateTrackList();
1280 }
1281 
1283 {
1284  QVariant data = QVariant::fromValue(track);
1286  if (item)
1287  {
1288  toggleTrackActive(item);
1289  }
1290 }
1291 
1293 {
1294  if (m_tracks->isEmpty() || !item)
1295  return;
1296 
1297  RipTrack *track = m_tracks->at(m_trackList->GetItemPos(item));
1298 
1299  if (!track->active && !track->isNew)
1300  {
1301  ShowConflictMenu(track);
1302  return;
1303  }
1304 
1305  track->active = !track->active;
1306 
1307  if (track->active)
1309  else
1311 
1313 }
1314 
1316 {
1317  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
1318 
1319  QString msg = tr("This track has been disabled because it is already "
1320  "present in the database.\n"
1321  "Do you want to permanently delete the existing "
1322  "file(s)?");
1323  MythDialogBox *menu = new MythDialogBox(msg, popupStack, "conflictmenu",
1324  true);
1325 
1326  if (menu->Create())
1327  popupStack->AddScreen(menu);
1328  else
1329  {
1330  delete menu;
1331  return;
1332  }
1333 
1334  menu->SetReturnEvent(this, "conflictmenu");
1335  menu->AddButton(tr("No, Cancel"));
1336  menu->AddButton(tr("Yes, Delete"), QVariant::fromValue(track));
1337  menu->AddButton(tr("Yes, Delete All"));
1338 }
1339 
1341 {
1342  QVector<RipTrack*>::iterator it;
1343  RipTrack *track;
1344  int length = 0;
1345 
1346  for (it = m_tracks->end() - 1; it == m_tracks->begin(); --it)
1347  {
1348  track = *it;
1349  if (track->active)
1350  {
1351  track->length = length + track->metadata->Length();
1352  length = 0;
1353  }
1354  else
1355  {
1356  track->length = 0;
1357  length += track->metadata->Length();
1358  }
1359  }
1360 }
1361 
1362 void Ripper::customEvent(QEvent* event)
1363 {
1364  if (event->type() == DialogCompletionEvent::kEventType)
1365  {
1366  DialogCompletionEvent *dce = static_cast<DialogCompletionEvent *>(event);
1367 
1368  if (dce->GetId() == "conflictmenu")
1369  {
1370  int buttonNum = dce->GetResult();
1371  RipTrack *track = qVariantValue<RipTrack *>(dce->GetData());
1372 
1373  switch (buttonNum)
1374  {
1375  case 0:
1376  // Do nothing
1377  break;
1378  case 1:
1379  if (deleteExistingTrack(track))
1380  {
1381  track->isNew = true;
1382  toggleTrackActive(track);
1383  }
1384  break;
1385  case 2:
1387  break;
1388  default:
1389  break;
1390  }
1391  }
1392 
1393  return;
1394  }
1395 
1396  MythUIType::customEvent(event);
1397 }
1398 
1399 
1401 
1402 RipStatus::RipStatus(MythScreenStack *parent, const QString &device,
1403  QVector<RipTrack*> *tracks, int quality)
1404  : MythScreenType(parent, "ripstatus"),
1405  m_tracks(tracks), m_quality(quality),
1406  m_CDdevice(device), m_overallText(NULL),
1407  m_trackText(NULL), m_statusText(NULL),
1408  m_overallPctText(NULL), m_trackPctText(NULL),
1409  m_overallProgress(NULL), m_trackProgress(NULL),
1410  m_ripperThread(NULL)
1411 {
1412 }
1413 
1415 {
1416  if (m_ripperThread)
1417  delete m_ripperThread;
1418 
1419  if (LCD *lcd = LCD::Get())
1420  lcd->switchToTime();
1421 }
1422 
1424 {
1425  if (!LoadWindowFromXML("music-ui.xml", "ripstatus", this))
1426  return false;
1427 
1428  m_overallText = dynamic_cast<MythUIText *>(GetChild("overall"));
1429  m_trackText = dynamic_cast<MythUIText *>(GetChild("track"));
1430  m_statusText = dynamic_cast<MythUIText *>(GetChild("status"));
1431  m_trackPctText = dynamic_cast<MythUIText *>(GetChild("trackpct"));
1432  m_overallPctText = dynamic_cast<MythUIText *>(GetChild("overallpct"));
1433 
1434  m_overallProgress = dynamic_cast<MythUIProgressBar *>(GetChild("overall_progress"));
1435  m_trackProgress = dynamic_cast<MythUIProgressBar *>(GetChild("track_progress"));
1436 
1437  BuildFocusList();
1438 
1439  startRip();
1440 
1441  return true;
1442 }
1443 
1444 bool RipStatus::keyPressEvent(QKeyEvent *event)
1445 {
1446  if (GetFocusWidget() && GetFocusWidget()->keyPressEvent(event))
1447  return true;
1448 
1449  bool handled = false;
1450  QStringList actions;
1451  handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions);
1452 
1453  for (int i = 0; i < actions.size() && !handled; i++)
1454  {
1455  QString action = actions[i];
1456  handled = true;
1457 
1458 
1459  if (action == "ESCAPE" &&
1461  {
1462  MythConfirmationDialog *dialog =
1463  ShowOkPopup(tr("Cancel ripping the CD?"), this, NULL, true);
1464  if (dialog)
1465  dialog->SetReturnEvent(this, "stop_ripping");
1466  }
1467  else
1468  handled = false;
1469  }
1470 
1471  if (!handled && MythScreenType::keyPressEvent(event))
1472  handled = true;
1473 
1474  return handled;
1475 }
1476 
1477 void RipStatus::customEvent(QEvent *event)
1478 {
1479  if (event->type() == DialogCompletionEvent::kEventType)
1480  {
1481  DialogCompletionEvent *dce = static_cast<DialogCompletionEvent *>(event);
1482 
1483  if (dce->GetId() == "stop_ripping" && dce->GetResult())
1484  {
1486  m_ripperThread->wait();
1487  Close();
1488  }
1489 
1490  return;
1491  }
1492 
1493  RipStatusEvent *rse = dynamic_cast<RipStatusEvent *> (event);
1494 
1495  if (!rse)
1496  return;
1497 
1498  if (event->type() == RipStatusEvent::kTrackTextEvent)
1499  {
1500  if (m_trackText)
1501  m_trackText->SetText(rse->text);
1502  }
1503  else if (event->type() == RipStatusEvent::kOverallTextEvent)
1504  {
1505  if (m_overallText)
1506  m_overallText->SetText(rse->text);
1507  }
1508  else if (event->type() == RipStatusEvent::kStatusTextEvent)
1509  {
1510  if (m_statusText)
1511  m_statusText->SetText(rse->text);
1512  }
1513  else if (event->type() == RipStatusEvent::kTrackProgressEvent)
1514  {
1515  if (m_trackProgress)
1516  m_trackProgress->SetUsed(rse->value);
1517  }
1518  else if (event->type() == RipStatusEvent::kTrackPercentEvent)
1519  {
1520  if (m_trackPctText)
1521  m_trackPctText->SetText(QString("%1%").arg(rse->value));
1522  }
1523  else if (event->type() == RipStatusEvent::kTrackStartEvent)
1524  {
1525  if (m_trackProgress)
1527  }
1528  else if (event->type() == RipStatusEvent::kOverallProgressEvent)
1529  {
1530  if (m_overallProgress)
1532  }
1533  else if (event->type() == RipStatusEvent::kOverallStartEvent)
1534  {
1535  if (m_overallProgress)
1537  }
1538  else if (event->type() == RipStatusEvent::kOverallPercentEvent)
1539  {
1540  if (m_overallPctText)
1541  m_overallPctText->SetText(QString("%1%").arg(rse->value));
1542  }
1543  else if (event->type() == RipStatusEvent::kFinishedEvent)
1544  {
1545  emit Result(true);
1546  Close();
1547  }
1548  else if (event->type() == RipStatusEvent::kEncoderErrorEvent)
1549  {
1550  ShowOkPopup(tr("The encoder failed to create the file.\n"
1551  "Do you have write permissions"
1552  " for the music directory?"));
1553  Close();
1554  }
1555  else
1556  {
1557  LOG(VB_GENERAL, LOG_ERR, "Received an unknown event type!");
1558  }
1559 }
1560 
1562 {
1563  if (m_ripperThread)
1564  delete m_ripperThread;
1565 
1567  m_ripperThread->start();
1568 }