MythTV  0.26-pre
decoderhandler.cpp
Go to the documentation of this file.
00001 // c/c++
00002 #include <unistd.h>
00003 #include <stdio.h>
00004 #include <assert.h>
00005 
00006 // qt
00007 #include <QApplication>
00008 #include <QUrl>
00009 #include <QFileInfo>
00010 
00011 // mythtv
00012 #include <mythdownloadmanager.h>
00013 #include <mythdirs.h>
00014 #include <mythlogging.h>
00015 #include <compat.h> // For random() on MINGW32
00016 #include <remotefile.h>
00017 #include <mythcorecontext.h>
00018 
00019 // mythmusic
00020 #include "decoderhandler.h"
00021 #include "decoder.h"
00022 #include "metadata.h"
00023 #include "streaminput.h"
00024 #include "shoutcast.h"
00025 
00026 /**********************************************************************/
00027 
00028 QEvent::Type DecoderHandlerEvent::Ready = (QEvent::Type) QEvent::registerEventType();
00029 QEvent::Type DecoderHandlerEvent::Meta = (QEvent::Type) QEvent::registerEventType();
00030 QEvent::Type DecoderHandlerEvent::Info = (QEvent::Type) QEvent::registerEventType();
00031 QEvent::Type DecoderHandlerEvent::OperationStart = (QEvent::Type) QEvent::registerEventType();
00032 QEvent::Type DecoderHandlerEvent::OperationStop = (QEvent::Type) QEvent::registerEventType();
00033 QEvent::Type DecoderHandlerEvent::Error = (QEvent::Type) QEvent::registerEventType();
00034 
00035 DecoderHandlerEvent::DecoderHandlerEvent(Type t, const Metadata &meta)
00036     : MythEvent(t), m_msg(NULL), m_meta(NULL)
00037 { 
00038     m_meta = new Metadata(meta);
00039 }
00040 
00041 DecoderHandlerEvent::~DecoderHandlerEvent(void)
00042 {
00043     if (m_msg)
00044         delete m_msg;
00045 
00046     if (m_meta)
00047         delete m_meta;
00048 }
00049 
00050 MythEvent* DecoderHandlerEvent::clone(void) const
00051 {
00052     DecoderHandlerEvent *result = new DecoderHandlerEvent(*this);
00053 
00054     if (m_msg)
00055         result->m_msg = new QString(*m_msg);
00056 
00057     if (m_meta)
00058         result->m_meta = new Metadata(*m_meta);
00059 
00060     return result;
00061 }
00062 
00063 /**********************************************************************/
00064 
00065 DecoderIOFactory::DecoderIOFactory(DecoderHandler *parent) 
00066 {
00067     m_handler = parent;
00068 }
00069 
00070 DecoderIOFactory::~DecoderIOFactory(void)
00071 {
00072 }
00073 
00074 void DecoderIOFactory::doConnectDecoder(const QString &format)
00075 {
00076     m_handler->doOperationStop();
00077     m_handler->doConnectDecoder(getUrl(), format);
00078 }
00079 
00080 Decoder *DecoderIOFactory::getDecoder(void)
00081 {
00082     return m_handler->getDecoder();
00083 }
00084 
00085 void DecoderIOFactory::doFailed(const QString &message)
00086 {
00087     m_handler->doOperationStop();
00088     m_handler->doFailed(getUrl(), message);
00089 }
00090 
00091 void DecoderIOFactory::doInfo(const QString &message)
00092 {
00093     m_handler->doInfo(message);
00094 }
00095 
00096 void DecoderIOFactory::doOperationStart(const QString &name)
00097 {
00098     m_handler->doOperationStart(name);
00099 }
00100 
00101 void DecoderIOFactory::doOperationStop(void)
00102 {
00103     m_handler->doOperationStop();
00104 }
00105 
00106 /**********************************************************************/
00107 
00108 DecoderIOFactoryFile::DecoderIOFactoryFile(DecoderHandler *parent)
00109     : DecoderIOFactory(parent), m_input (NULL)
00110 {
00111 }
00112 
00113 DecoderIOFactoryFile::~DecoderIOFactoryFile(void)
00114 {
00115     if (m_input)
00116         delete m_input;
00117 }
00118 
00119 QIODevice* DecoderIOFactoryFile::takeInput(void)
00120 {
00121     QIODevice *result = m_input;
00122     m_input = NULL;
00123     return result;
00124 }
00125 
00126 void DecoderIOFactoryFile::start(void)
00127 {
00128     QString sourcename = getMetadata().Filename();
00129 
00130     LOG(VB_PLAYBACK, LOG_INFO,
00131         QString("DecoderIOFactory: Opening Local File %1").arg(sourcename));
00132 
00133     m_input = new QFile(sourcename);
00134     doConnectDecoder(getUrl().toLocalFile());
00135 }
00136 
00137 /**********************************************************************/
00138 
00139 DecoderIOFactorySG::DecoderIOFactorySG(DecoderHandler *parent)
00140     : DecoderIOFactory(parent), m_input(NULL)
00141 {
00142 }
00143 
00144 DecoderIOFactorySG::~DecoderIOFactorySG(void)
00145 {
00146     if (m_input)
00147         delete m_input;
00148 }
00149 
00150 QIODevice* DecoderIOFactorySG::takeInput(void)
00151 {
00152     QIODevice *result = m_input;
00153     m_input = NULL;
00154     return result;
00155 }
00156 
00157 void DecoderIOFactorySG::start(void)
00158 {
00159     QString url = getUrl().toString();
00160     LOG(VB_PLAYBACK, LOG_INFO,
00161         QString("DecoderIOFactorySG: Opening Myth URL %1").arg(url));
00162     m_input = new MusicSGIODevice(url);
00163     doConnectDecoder(getUrl().path());
00164 }
00165 
00166 /**********************************************************************/
00167 
00168 DecoderIOFactoryUrl::DecoderIOFactoryUrl(DecoderHandler *parent) : DecoderIOFactory(parent)
00169 {
00170     m_accessManager = new QNetworkAccessManager(this);
00171     m_input = new MusicIODevice();
00172     connect(m_input, SIGNAL(freeSpaceAvailable()), SLOT(readyRead()));
00173 
00174     m_input->open(QIODevice::ReadWrite);
00175 
00176     m_bytesWritten = 0;
00177     m_redirectCount = 0;
00178 }
00179 
00180 DecoderIOFactoryUrl::~DecoderIOFactoryUrl(void)
00181 {
00182     doClose();
00183 
00184     m_accessManager->deleteLater();
00185 
00186     if (m_input)
00187         delete m_input;
00188 }
00189 
00190 QIODevice* DecoderIOFactoryUrl::takeInput(void) 
00191 {
00192     QIODevice *result = m_input;
00193     //m_input = NULL;
00194     return result;
00195 }
00196 
00197 void DecoderIOFactoryUrl::start(void)
00198 {
00199     LOG(VB_PLAYBACK, LOG_INFO,
00200         QString("DecoderIOFactory: Url %1").arg(getUrl().toString()));
00201 
00202     m_started = false;
00203 
00204     doOperationStart("Fetching remote file");
00205 
00206     m_reply = m_accessManager->get(QNetworkRequest(getUrl()));
00207 
00208     connect(m_reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
00209     connect(m_accessManager, SIGNAL(finished(QNetworkReply*)),
00210             this, SLOT(replyFinished(QNetworkReply*)));
00211 }
00212 
00213 void DecoderIOFactoryUrl::stop(void)
00214 {
00215     doClose();
00216 }
00217 
00218 void DecoderIOFactoryUrl::replyFinished(QNetworkReply *reply)
00219 {
00220     if (reply->error() != QNetworkReply::NoError) 
00221     {
00222         doFailed("Cannot retrieve remote file.");
00223         return;
00224     }
00225 
00226     QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
00227 
00228     if (!possibleRedirectUrl.isEmpty() && (m_redirectedURL != possibleRedirectUrl))
00229     {
00230         LOG(VB_PLAYBACK, LOG_INFO,
00231             QString("DecoderIOFactory: Got redirected to %1")
00232                 .arg(possibleRedirectUrl.toString()));
00233 
00234         m_redirectCount++;
00235 
00236         if (m_redirectCount > MaxRedirects)
00237         {
00238             doFailed("Too many redirects");
00239         }
00240         else
00241         {
00242             setUrl(possibleRedirectUrl);
00243             m_redirectedURL = possibleRedirectUrl;
00244             start();
00245         }
00246 
00247         return;
00248     }
00249 
00250     m_redirectedURL.clear();
00251 
00252     if (!m_started)
00253         doStart();
00254 }
00255 
00256 void DecoderIOFactoryUrl::readyRead(void)
00257 {
00258     int available = DecoderIOFactory::DefaultBufferSize - m_input->bytesAvailable();
00259     QByteArray data = m_reply->read(available);
00260 
00261     m_bytesWritten += data.size();
00262     m_input->writeData(data.data(), data.size());
00263 
00264     if (!m_started && m_bytesWritten > DecoderIOFactory::DefaultPrebufferSize)
00265     {
00266         m_reply->setReadBufferSize(DecoderIOFactory::DefaultPrebufferSize);
00267         doStart();
00268     }
00269 
00270 #if 0
00271     LOG(VB_GENERAL, LOG_DEBUG,
00272         QString("DecoderIOFactoryUrl::readyRead file size: %1")
00273             .arg(m_bytesWritten));
00274 #endif
00275 }
00276 
00277 void DecoderIOFactoryUrl::doStart(void)
00278 {
00279     doConnectDecoder(getUrl().toString());
00280     m_started = true;
00281 }
00282 
00283 void DecoderIOFactoryUrl::doClose(void)
00284 {
00285     if (m_input && m_input->isOpen())
00286         m_input->close();
00287 }
00288 
00289 /**********************************************************************/
00290 
00291 DecoderHandler::DecoderHandler(void) :
00292     m_state(STOPPED),
00293     m_playlist_pos(0),
00294     m_io_factory(NULL),
00295     m_decoder(NULL),
00296     m_meta(NULL),
00297     m_op(false),
00298     m_redirects(0)
00299 {
00300 }
00301 
00302 DecoderHandler::~DecoderHandler(void)
00303 {
00304     stop();
00305 }
00306 
00307 void DecoderHandler::start(Metadata *mdata)
00308 {
00309     m_state = LOADING;
00310 
00311     m_playlist.clear();
00312     m_meta = mdata;
00313     m_playlist_pos = -1;
00314     m_redirects = 0;
00315 
00316     QUrl url;
00317     if (QFileInfo(mdata->Filename()).isAbsolute())
00318         url = QUrl::fromLocalFile(mdata->Filename());
00319     else
00320         url.setUrl(mdata->Filename());
00321 
00322     bool result = createPlaylist(url);
00323     if (m_state == LOADING && result)
00324     {
00325         for (int ii = 0; ii < m_playlist.size(); ii++)
00326             LOG(VB_PLAYBACK, LOG_INFO, QString("Track %1 = %2")
00327                 .arg(ii) .arg(m_playlist.get(ii)->File()));
00328         next();
00329     }
00330     else
00331     {
00332         if (m_state != STOPPED)
00333         {
00334             doFailed(url, "Could not get playlist");
00335         }
00336     }
00337 }
00338 
00339 void DecoderHandler::error(const QString &e)
00340 {
00341     QString *str = new QString(e);
00342     DecoderHandlerEvent ev(DecoderHandlerEvent::Error, str);
00343     dispatch(ev);
00344 }
00345 
00346 bool DecoderHandler::done(void)
00347 {
00348     if (m_state == STOPPED)
00349         return true;
00350 
00351     if (m_playlist_pos + 1 >= m_playlist.size())
00352     {
00353         m_state = STOPPED;
00354         return true;
00355     }
00356 
00357     return false;
00358 }
00359 
00360 bool DecoderHandler::next(void)
00361 {
00362     if (done())
00363         return false;
00364 
00365     if (m_meta && m_meta->Format() == "cast")
00366     {
00367         m_playlist_pos = random() % m_playlist.size();
00368     }
00369     else
00370     {
00371         m_playlist_pos++;
00372     }
00373 
00374     PlayListFileEntry *entry = m_playlist.get(m_playlist_pos);
00375 
00376     QUrl url;
00377     if (QFileInfo(entry->File()).isAbsolute())
00378         url = QUrl::fromLocalFile(entry->File());
00379     else
00380         url.setUrl(entry->File());
00381 
00382     LOG(VB_PLAYBACK, LOG_INFO, QString("Now playing '%1'").arg(url.toString()));
00383 
00384     deleteIOFactory();
00385     createIOFactory(url);
00386 
00387     if (! haveIOFactory())
00388         return false;
00389 
00390     getIOFactory()->addListener(this);
00391     getIOFactory()->setUrl(url);
00392     getIOFactory()->setMeta(m_meta);
00393     getIOFactory()->start();
00394     m_state = ACTIVE;
00395 
00396     return true;
00397 }
00398 
00399 void DecoderHandler::stop(void)
00400 {
00401     LOG(VB_PLAYBACK, LOG_INFO, QString("DecoderHandler: Stopping decoder"));
00402 
00403     if (m_decoder && m_decoder->isRunning())
00404     {
00405         m_decoder->lock();
00406         m_decoder->stop();
00407         m_decoder->unlock();
00408     }
00409 
00410     if (m_decoder)
00411     {
00412         m_decoder->lock();
00413         m_decoder->cond()->wakeAll();
00414         m_decoder->unlock();
00415     }
00416 
00417     if (m_decoder)
00418     {
00419         m_decoder->wait();
00420         //delete m_decoder->input(); // TODO: need to sort out who is responsible for the input
00421         delete m_decoder;
00422         m_decoder = NULL;
00423     }
00424 
00425     deleteIOFactory();
00426     doOperationStop();
00427 
00428     m_state = STOPPED;
00429 }
00430 
00431 void DecoderHandler::customEvent(QEvent *e)
00432 {
00433     if (class DecoderHandlerEvent *event = dynamic_cast<DecoderHandlerEvent*>(e)) 
00434     {
00435         // Proxy all DecoderHandlerEvents
00436         return dispatch(*event);
00437     }
00438 }
00439 
00440 bool DecoderHandler::createPlaylist(const QUrl &url)
00441 {
00442     QString extension = QFileInfo(url.path()).suffix();
00443     LOG(VB_NETWORK, LOG_INFO,
00444         QString("File %1 has extension %2")
00445             .arg(QFileInfo(url.path()).fileName()).arg(extension));
00446 
00447     if (extension == "pls" || extension == "m3u")
00448     {
00449         if (url.scheme() == "file" || QFileInfo(url.toString()).isAbsolute())
00450             return createPlaylistFromFile(url);
00451         else
00452             return createPlaylistFromRemoteUrl(url);
00453     }
00454 
00455     return createPlaylistForSingleFile(url);
00456 }
00457 
00458 bool DecoderHandler::createPlaylistForSingleFile(const QUrl &url)
00459 {
00460     PlayListFileEntry *entry = new PlayListFileEntry;
00461 
00462     if (url.scheme() == "file" || QFileInfo(url.toString()).isAbsolute())
00463         entry->setFile(url.toLocalFile());
00464     else
00465         entry->setFile(url.toString());
00466 
00467     m_playlist.add(entry);
00468 
00469     return m_playlist.size() > 0;
00470 }
00471 
00472 bool DecoderHandler::createPlaylistFromFile(const QUrl &url)
00473 {
00474     QFile f(QFileInfo(url.path()).absolutePath() + "/" + QFileInfo(url.path()).fileName());
00475     f.open(QIODevice::ReadOnly);
00476     QTextStream stream(&f);
00477 
00478     QString extension = QFileInfo(url.path()).suffix().toLower();
00479 
00480     if (PlayListFile::parse(&m_playlist, &stream, extension) <= 0)
00481         return false;
00482 
00483     return m_playlist.size() > 0;
00484 }
00485 
00486 bool DecoderHandler::createPlaylistFromRemoteUrl(const QUrl &url) 
00487 {
00488     LOG(VB_NETWORK, LOG_INFO,
00489         QString("Retrieving playlist from '%1'").arg(url.toString()));
00490 
00491     doOperationStart("Retrieving playlist");
00492 
00493     QByteArray data;
00494 
00495     if (!GetMythDownloadManager()->download(url.toString(), &data))
00496     {
00497         LOG(VB_GENERAL, LOG_ERR, QString("DecoderHandler:: Failed to download playlist from: %1").arg(url.toString()));
00498         doOperationStop();
00499         return false;
00500     }
00501 
00502     doOperationStop();
00503 
00504     QTextStream stream(&data, QIODevice::ReadOnly);
00505 
00506     QString extension = QFileInfo(url.path()).suffix().toLower();
00507 
00508     bool result = PlayListFile::parse(&m_playlist, &stream, extension) > 0;
00509 
00510     return result;
00511 }
00512 
00513 void DecoderHandler::doConnectDecoder(const QUrl &url, const QString &format) 
00514 {
00515     if (m_decoder && !m_decoder->factory()->supports(format)) 
00516     {
00517         delete m_decoder;
00518         m_decoder = NULL;
00519     }
00520 
00521     if (!m_decoder)
00522     {
00523         if ((m_decoder = Decoder::create(format, NULL, NULL, true)) == NULL)
00524         {
00525             doFailed(url, QString("No decoder for this format '%1'").arg(format));
00526             return;
00527         }
00528     }
00529 
00530     m_decoder->setInput(getIOFactory()->takeInput());
00531     m_decoder->setFilename(url.toString());
00532 
00533     DecoderHandlerEvent ev(DecoderHandlerEvent::Ready);
00534     dispatch(ev);
00535 }
00536 
00537 void DecoderHandler::doFailed(const QUrl &url, const QString &message)
00538 {
00539     LOG(VB_NETWORK, LOG_ERR,
00540         QString("DecoderHandler: Unsupported file format: '%1' - %2")
00541             .arg(url.toString()).arg(message));
00542     DecoderHandlerEvent ev(DecoderHandlerEvent::Error, new QString(message));
00543     dispatch(ev);
00544 }
00545 
00546 void DecoderHandler::doInfo(const QString &message)
00547 {
00548     DecoderHandlerEvent ev(DecoderHandlerEvent::Info, new QString(message));
00549     dispatch(ev);
00550 }
00551 
00552 void DecoderHandler::doOperationStart(const QString &name)
00553 {
00554     m_op = true;
00555     DecoderHandlerEvent ev(DecoderHandlerEvent::OperationStart, new QString(name));
00556     dispatch(ev);
00557 }
00558 
00559 void DecoderHandler::doOperationStop(void)
00560 {
00561     if (!m_op)
00562         return;
00563 
00564     m_op = false;
00565     DecoderHandlerEvent ev(DecoderHandlerEvent::OperationStop);
00566     dispatch(ev);
00567 }
00568 
00569 void DecoderHandler::createIOFactory(const QUrl &url)
00570 {
00571     if (haveIOFactory())
00572         deleteIOFactory();
00573 
00574     if (url.scheme() == "myth")
00575         m_io_factory = new DecoderIOFactorySG(this);
00576     else if (m_meta && m_meta->Format() == "cast")
00577         m_io_factory = new DecoderIOFactoryShoutCast(this);
00578     else if (url.scheme() == "http")
00579         m_io_factory = new DecoderIOFactoryUrl(this);
00580     else
00581         m_io_factory = new DecoderIOFactoryFile(this);
00582 }
00583 
00584 void DecoderHandler::deleteIOFactory(void)
00585 {
00586     if (!haveIOFactory())
00587         return;
00588 
00589     if (m_state == ACTIVE)
00590         m_io_factory->stop();
00591 
00592     m_io_factory->removeListener(this);
00593     m_io_factory->disconnect();
00594     m_io_factory->deleteLater();
00595     m_io_factory = NULL;
00596 }
00597 
00598 /**********************************************************************/
00599 
00600 qint64 MusicBuffer::read(char *data, qint64 max, bool doRemove)
00601 {
00602     QMutexLocker holder (&m_mutex);
00603     const char *buffer_data = m_buffer.data();
00604 
00605     if (max > m_buffer.size())
00606         max = m_buffer.size();
00607 
00608     memcpy(data, buffer_data, max);
00609 
00610     if (doRemove)
00611         m_buffer.remove(0, max);
00612 
00613     return max;
00614 }
00615 
00616 qint64 MusicBuffer::read(QByteArray &data, qint64 max, bool doRemove)
00617 {
00618     QMutexLocker holder (&m_mutex);
00619     const char *buffer_data = m_buffer.data();
00620 
00621     if (max > m_buffer.size())
00622         max = m_buffer.size();
00623 
00624     data.append(buffer_data, max);
00625 
00626     if (doRemove)
00627         m_buffer.remove(0, max);
00628 
00629     return max;
00630 }
00631 
00632 void MusicBuffer::write(const char *data, uint sz)
00633 {
00634     if (sz == 0)
00635         return;
00636 
00637     QMutexLocker holder(&m_mutex);
00638     m_buffer.append(data, sz);
00639 }
00640 
00641 void MusicBuffer::write(QByteArray &array)
00642 {
00643     if (array.size() == 0)
00644         return;
00645 
00646     QMutexLocker holder(&m_mutex);
00647     m_buffer.append(array);
00648 }
00649 
00650 void MusicBuffer::remove(int index, int len)
00651 {
00652     QMutexLocker holder(&m_mutex);
00653     m_buffer.remove(index, len);
00654 }
00655 
00656 /**********************************************************************/
00657 
00658 MusicIODevice::MusicIODevice(void)
00659 {
00660     m_buffer = new MusicBuffer;
00661     setOpenMode(ReadWrite);
00662 }
00663 
00664 MusicIODevice::~MusicIODevice(void)
00665 {
00666     delete m_buffer;
00667 }
00668 
00669 bool MusicIODevice::open(int)
00670 {
00671     return true;
00672 }
00673 
00674 qint64 MusicIODevice::size(void) const
00675 {
00676     return m_buffer->readBufAvail(); 
00677 }
00678 
00679 qint64 MusicIODevice::readData(char *data, qint64 maxlen)
00680 {
00681     qint64 res = m_buffer->read(data, maxlen);
00682     emit freeSpaceAvailable();
00683     return res;
00684 }
00685 
00686 qint64 MusicIODevice::writeData(const char *data, qint64 sz)
00687 {
00688     m_buffer->write(data, sz);
00689     return sz;
00690 }
00691 
00692 qint64 MusicIODevice::bytesAvailable(void) const
00693 {
00694     return m_buffer->readBufAvail();
00695 }
00696 
00697 int MusicIODevice::getch(void)
00698 {
00699     assert(0);
00700     return -1;
00701 }
00702 
00703 int MusicIODevice::putch(int)
00704 {
00705     assert(0);
00706     return -1;
00707 }
00708 
00709 int MusicIODevice::ungetch(int)
00710 {
00711     assert(0);
00712     return -1;
00713 }
00714 
00715 /**********************************************************************/
00716 
00717 MusicSGIODevice::MusicSGIODevice(const QString &url)
00718 {
00719     m_remotefile = new RemoteFile(url);
00720     m_remotefile->Open();
00721 
00722     setOpenMode(ReadWrite);
00723 }
00724 
00725 MusicSGIODevice::~MusicSGIODevice(void)
00726 {
00727     m_remotefile->Close();
00728     delete m_remotefile;
00729 }
00730 
00731 bool MusicSGIODevice::open(int)
00732 {
00733     return true;
00734 }
00735 
00736 bool MusicSGIODevice::seek(qint64 pos)
00737 {
00738     long int newPos = -1;
00739 
00740     if (m_remotefile)
00741         newPos = m_remotefile->Seek(pos, 0);
00742 
00743     return (newPos == pos);
00744 }
00745 
00746 qint64 MusicSGIODevice::size(void) const
00747 {
00748     return m_remotefile->GetFileSize();
00749 }
00750 
00751 qint64 MusicSGIODevice::readData(char *data, qint64 maxlen)
00752 {
00753     qint64 res = m_remotefile->Read(data, maxlen);
00754     return res;
00755 }
00756 
00757 qint64 MusicSGIODevice::writeData(const char *data, qint64 sz)
00758 {
00759     m_remotefile->Write(data, sz);
00760     return sz;
00761 }
00762 
00763 qint64 MusicSGIODevice::bytesAvailable(void) const
00764 {
00765     return m_remotefile->GetFileSize();
00766 }
00767 
00768 int MusicSGIODevice::getch(void)
00769 {
00770     assert(0);
00771     return -1;
00772 }
00773 
00774 int MusicSGIODevice::putch(int)
00775 {
00776     assert(0);
00777     return -1;
00778 }
00779 
00780 int MusicSGIODevice::ungetch(int)
00781 {
00782     assert(0);
00783     return -1;
00784 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends