Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Support reading of ID3v2 or ID3v1 tags in FLAC

If an ID3 tag exists then we will read and write to that instead of the
Vorbis comment tag. This includes support for albumart in ID3v2 tags
found in FLAC files.

The implementation is a little messy but I plan to refactor the metaio
classes and this will do for now.
  • Loading branch information...
commit 6e37202bb8ffcbc35e62887a4cb9887afa5ab7e5 1 parent e03fc09
@stuartm stuartm authored
View
8 mythplugins/mythmusic/mythmusic/avfdecoder.cpp
@@ -508,7 +508,13 @@ MetaIO* avfDecoder::doCreateTagger(void)
else if (extension == "ogg" || extension == "oga")
return new MetaIOOggVorbis();
else if (extension == "flac")
- return new MetaIOFLACVorbis();
+ {
+ MetaIOID3 *file = new MetaIOID3();
+ if (file->TagExists(filename))
+ return file;
+ else
+ return new MetaIOFLACVorbis();
+ }
else if (extension == "m4a")
return new MetaIOMP4();
else if (extension == "wv")
View
7 mythplugins/mythmusic/mythmusic/metadata.cpp
@@ -917,7 +917,12 @@ MetaIO* Metadata::getTagger(void)
else if (extension == "ogg" || extension == "oga")
return &metaIOOggVorbis;
else if (extension == "flac")
- return &metaIOFLACVorbis;
+ {
+ if (metaIOID3.TagExists(m_filename))
+ return &metaIOID3;
+ else
+ return &metaIOFLACVorbis;
+ }
else if (extension == "m4a")
return &metaIOMP4;
else if (extension == "wv")
View
6 mythplugins/mythmusic/mythmusic/metaio.h
@@ -101,6 +101,12 @@ class MetaIO
void readFromFilename(Metadata *metadata);
+ virtual bool TagExists(const QString &filename)
+ {
+ (void)filename;
+ return false;
+ }
+
protected:
private:
View
28 mythplugins/mythmusic/mythmusic/metaioflacvorbis.cpp
@@ -52,7 +52,7 @@ bool MetaIOFLACVorbis::write(Metadata* mdata)
if (!flacfile)
return false;
- TagLib::Ogg::XiphComment *tag = flacfile->xiphComment();
+ TagLib::Ogg::XiphComment *tag = flacfile->xiphComment(true);
if (!tag)
{
@@ -136,15 +136,27 @@ Metadata* MetaIOFLACVorbis::read(QString filename)
metadata->setCompilation(compilation);
if (metadata->Length() <= 0)
- {
- TagLib::FileRef *fileref = new TagLib::FileRef(flacfile);
- metadata->setLength(getTrackLength(fileref));
- // FileRef takes ownership of flacfile, and is responsible for it's
- // deletion. Messy.
- delete fileref;
- }
+ metadata->setLength(getTrackLength(flacfile));
else
delete flacfile;
return metadata;
}
+
+bool MetaIOFLACVorbis::TagExists(const QString &filename)
+{
+ TagLib::FLAC::File *flacfile = OpenFile(filename);
+
+ if (!flacfile)
+ return false;
+
+ TagLib::Ogg::XiphComment *tag = flacfile->xiphComment(false);
+
+ bool retval = false;
+ if (tag && !tag->isEmpty())
+ retval = true;
+
+ delete flacfile;
+
+ return retval;
+}
View
2  mythplugins/mythmusic/mythmusic/metaioflacvorbis.h
@@ -27,6 +27,8 @@ class MetaIOFLACVorbis : public MetaIOTagLib
bool write(Metadata* mdata);
Metadata* read(QString filename);
+ virtual bool TagExists(const QString &filename);
+
private:
TagLib::FLAC::File *OpenFile(const QString &filename);
};
View
297 mythplugins/mythmusic/mythmusic/metaioid3.cpp
@@ -2,19 +2,26 @@
#include "metaioid3.h"
#include "metadata.h"
-// Libmyth
+// Libmythbase
#include <mythlogging.h>
#include <set>
+// Taglib
+#include <flacfile.h>
+#include <mpegfile.h>
+
const String email = "music@mythtv.org"; // TODO username/ip/hostname?
MetaIOID3::MetaIOID3(void)
- : MetaIOTagLib()
+ : MetaIOTagLib(),
+ m_file(NULL), m_fileType(kMPEG)
+
{
}
MetaIOID3::~MetaIOID3(void)
{
+ CloseFile();
}
/*!
@@ -23,18 +30,124 @@ MetaIOID3::~MetaIOID3(void)
* \param filename The filename
* \returns A taglib file object for this format
*/
-TagLib::MPEG::File *MetaIOID3::OpenFile(const QString &filename)
+bool MetaIOID3::OpenFile(const QString &filename, bool forWriting)
{
- QByteArray fname = filename.toLocal8Bit();
- TagLib::MPEG::File *mpegfile = new TagLib::MPEG::File(fname.constData());
+
+
+ // Check if file is already opened
+ if (m_file && (m_filename == filename) &&
+ (!forWriting || !m_file->readOnly()))
+ return true;
+
+ if (m_file)
+ {
+ LOG(VB_FILE, LOG_DEBUG,
+ QString("MetaIO switch file: %1 New File: %2 Type: %3")
+ .arg(m_filename)
+ .arg(filename)
+ .arg(m_fileType));
+ }
+
+ // If a file is open but it's not the requested file then close it first
+ if (m_file)
+ CloseFile();
+
+ m_filename = filename;
+
+ QString extension = m_filename.section('.', -1);
+
+ if (extension.toLower() == "flac")
+ m_fileType = kFLAC;
+ else if (extension.toLower() == "mp3")
+ m_fileType = kMPEG;
+ else
+ return false;
+
+ QByteArray fname = m_filename.toLocal8Bit();
+ // Open the file
+ switch (m_fileType)
+ {
+ case kMPEG :
+ m_file = new TagLib::MPEG::File(fname.constData());
+ break;
+ case kFLAC :
+ m_file = new TagLib::FLAC::File(fname.constData());
+ break;
+ }
- if (!mpegfile->isOpen())
+ // If the requested file could not be opened then close it and return false
+ if (!m_file->isOpen() || (forWriting && m_file->readOnly()))
{
- delete mpegfile;
- mpegfile = NULL;
+ if (m_file->isOpen())
+ LOG(VB_FILE, LOG_NOTICE,
+ QString("Could not open file for writing: %1").arg(m_filename));
+ else
+ LOG(VB_FILE, LOG_ERR,
+ QString("Could not open file: %1").arg(m_filename));
+
+ CloseFile();
+ return false;
}
- return mpegfile;
+ return true;
+}
+
+bool MetaIOID3::SaveFile()
+{
+ if (!m_file)
+ return false;
+
+ bool retval = m_file->save();
+
+ return retval;
+}
+
+void MetaIOID3::CloseFile()
+{
+ LOG(VB_FILE, LOG_DEBUG, QString("MetaIO Close file: %1")
+ .arg(m_file->name()));
+ delete m_file;
+ m_file = NULL;
+ m_fileType = kMPEG;
+ m_filename.clear();
+}
+
+TagLib::ID3v2::Tag* MetaIOID3::GetID3v2Tag(bool create)
+{
+ if (!m_file)
+ return NULL;
+
+ TagLib::ID3v2::Tag *tag = NULL;
+ switch (m_fileType)
+ {
+ case kMPEG :
+ tag = (static_cast<TagLib::MPEG::File*>(m_file))->ID3v2Tag(create);
+ break;
+ case kFLAC :
+ tag = (static_cast<TagLib::FLAC::File*>(m_file))->ID3v2Tag(create);
+ break;
+ }
+
+ return tag;
+}
+
+TagLib::ID3v1::Tag* MetaIOID3::GetID3v1Tag(bool create)
+{
+ if (!m_file)
+ return NULL;
+
+ TagLib::ID3v1::Tag *tag = NULL;
+ switch (m_fileType)
+ {
+ case kMPEG :
+ tag = (static_cast<TagLib::MPEG::File*>(m_file))->ID3v1Tag(create);
+ break;
+ case kFLAC :
+ // Flac doesn't support ID3v1
+ break;
+ }
+
+ return tag;
}
/*!
@@ -42,18 +155,13 @@ TagLib::MPEG::File *MetaIOID3::OpenFile(const QString &filename)
*/
bool MetaIOID3::write(Metadata* mdata)
{
- TagLib::MPEG::File *mpegfile = OpenFile(mdata->Filename());
-
- if (!mpegfile)
+ if (!OpenFile(mdata->Filename()), true)
return false;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return false;
- }
WriteGenericMetadata(tag, mdata);
@@ -111,11 +219,10 @@ bool MetaIOID3::write(Metadata* mdata)
tpe2frame->setText(QStringToTString(mdata->CompilationArtist()));
}
- bool result = mpegfile->save();
-
- delete mpegfile;
+ if (!SaveFile())
+ return false;
- return result;
+ return true;
}
/*!
@@ -123,29 +230,19 @@ bool MetaIOID3::write(Metadata* mdata)
*/
Metadata *MetaIOID3::read(QString filename)
{
- TagLib::MPEG::File *mpegfile = OpenFile(filename);
-
- if (!mpegfile)
+ if (!OpenFile(filename))
return NULL;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag(true); // Create tag if none are found
- if (!tag)
- {
- delete mpegfile;
- return NULL;
- }
-
- // if there is no ID3v2 tag, try to read the ID3v1 tag and copy it to the ID3v2 tag structure
+ // if there is no ID3v2 tag, try to read the ID3v1 tag and copy it to
+ // the ID3v2 tag structure
if (tag->isEmpty())
{
- TagLib::ID3v1::Tag *tag_v1 = mpegfile->ID3v1Tag();
+ TagLib::ID3v1::Tag *tag_v1 = GetID3v1Tag();
if (!tag_v1)
- {
- delete mpegfile;
return NULL;
- }
if (!tag_v1->isEmpty())
{
@@ -219,13 +316,8 @@ Metadata *MetaIOID3::read(QString filename)
// 27 hours
metadata->setCompilation(compilation);
-
- TagLib::FileRef *fileref = new TagLib::FileRef(mpegfile);
- metadata->setLength(getTrackLength(fileref));
- // FileRef takes ownership of mpegfile, and is responsible for it's
- // deletion. Messy.
- delete fileref;
-
+ metadata->setLength(getTrackLength(m_file));
+
return metadata;
}
@@ -265,15 +357,13 @@ QImage* MetaIOID3::getAlbumArt(QString filename, ImageType type)
}
QByteArray fname = filename.toLocal8Bit();
- TagLib::MPEG::File *mpegfile = new TagLib::MPEG::File(fname.constData());
- if (mpegfile)
+ if (OpenFile(fname.constData()))
{
- if (mpegfile->isOpen()
- && !mpegfile->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
+ if (tag && !tag->frameListMap()["APIC"].isEmpty())
{
- TagLib::ID3v2::FrameList apicframes =
- mpegfile->ID3v2Tag()->frameListMap()["APIC"];
+ TagLib::ID3v2::FrameList apicframes = tag->frameListMap()["APIC"];
for(TagLib::ID3v2::FrameList::Iterator it = apicframes.begin();
it != apicframes.end(); ++it)
@@ -288,8 +378,6 @@ QImage* MetaIOID3::getAlbumArt(QString filename, ImageType type)
}
}
}
-
- delete mpegfile;
}
delete picture;
@@ -307,21 +395,15 @@ AlbumArtList MetaIOID3::getAlbumArtList(const QString &filename)
{
AlbumArtList imageList;
QByteArray fname = filename.toLocal8Bit();
- TagLib::MPEG::File *mpegfile = new TagLib::MPEG::File(fname.constData());
- if (mpegfile)
+ if (OpenFile(fname.constData()))
{
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return imageList;
- }
imageList = readAlbumArt(tag);
-
- delete mpegfile;
}
return imageList;
@@ -368,7 +450,8 @@ AlbumArtList MetaIOID3::readAlbumArt(TagLib::ID3v2::Tag *tag)
art->embedded = true;
- QString ext = getExtFromMimeType(TStringToQString(frame->mimeType()).toLower());
+ QString ext = getExtFromMimeType(
+ TStringToQString(frame->mimeType()).toLower());
switch (frame->type())
{
@@ -454,7 +537,8 @@ AttachedPictureFrame* MetaIOID3::findAPIC(TagLib::ID3v2::Tag *tag,
*
* \Note We always save the image in JPEG format
*/
-bool MetaIOID3::writeAlbumArt(const QString &filename, const AlbumArtImage *albumart)
+bool MetaIOID3::writeAlbumArt(const QString &filename,
+ const AlbumArtImage *albumart)
{
if (filename.isEmpty() || !albumart)
return false;
@@ -486,20 +570,17 @@ bool MetaIOID3::writeAlbumArt(const QString &filename, const AlbumArtImage *albu
break;
}
- TagLib::MPEG::File *mpegfile = OpenFile(filename);
-
- if (!mpegfile)
+ QByteArray fname = filename.toLocal8Bit();
+ if (!OpenFile(fname.constData(), true))
return false;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return false;
- }
- AttachedPictureFrame *apic = findAPIC(tag, type, QStringToTString(albumart->description));
+ AttachedPictureFrame *apic = findAPIC(tag, type,
+ QStringToTString(albumart->description));
if (!apic)
{
@@ -517,8 +598,8 @@ bool MetaIOID3::writeAlbumArt(const QString &filename, const AlbumArtImage *albu
apic->setPicture(bytevector);
apic->setDescription(QStringToTString(albumart->description));
- mpegfile->save();
- delete mpegfile;
+ if (!SaveFile())
+ return false;
return true;
}
@@ -530,7 +611,8 @@ bool MetaIOID3::writeAlbumArt(const QString &filename, const AlbumArtImage *albu
* \param albumart The Album Art image to remove
* \returns True if successful
*/
-bool MetaIOID3::removeAlbumArt(const QString &filename, const AlbumArtImage *albumart)
+bool MetaIOID3::removeAlbumArt(const QString &filename,
+ const AlbumArtImage *albumart)
{
if (filename.isEmpty() || !albumart)
return false;
@@ -555,35 +637,30 @@ bool MetaIOID3::removeAlbumArt(const QString &filename, const AlbumArtImage *alb
break;
}
- TagLib::MPEG::File *mpegfile = OpenFile(filename);
-
- if (!mpegfile)
+ QByteArray fname = filename.toLocal8Bit();
+ if (!OpenFile(fname.constData(), true))
return false;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return false;
- }
- AttachedPictureFrame *apic = findAPIC(tag, type, QStringToTString(albumart->description));
+ AttachedPictureFrame *apic = findAPIC(tag, type,
+ QStringToTString(albumart->description));
if (!apic)
- {
- delete mpegfile;
return false;
- }
tag->removeFrame(apic);
- mpegfile->save();
- delete mpegfile;
+ if (!SaveFile())
+ return false;
return true;
}
-bool MetaIOID3::changeImageType(const QString &filename, const AlbumArtImage* albumart,
+bool MetaIOID3::changeImageType(const QString &filename,
+ const AlbumArtImage* albumart,
ImageType newType)
{
if (!albumart)
@@ -612,25 +689,19 @@ bool MetaIOID3::changeImageType(const QString &filename, const AlbumArtImage* al
break;
}
- TagLib::MPEG::File *mpegfile = OpenFile(filename);
-
- if (!mpegfile)
+ QByteArray fname = filename.toLocal8Bit();
+ if (!OpenFile(fname.constData(), true))
return false;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return false;
- }
- AttachedPictureFrame *apic = findAPIC(tag, type, QStringToTString(albumart->description));
+ AttachedPictureFrame *apic = findAPIC(tag, type,
+ QStringToTString(albumart->description));
if (!apic)
- {
- delete mpegfile;
return false;
- }
// set the new image type
switch (newType)
@@ -652,8 +723,8 @@ bool MetaIOID3::changeImageType(const QString &filename, const AlbumArtImage* al
break;
}
- mpegfile->save();
- delete mpegfile;
+ if (!SaveFile())
+ return false;
return true;
}
@@ -726,23 +797,20 @@ bool MetaIOID3::writeVolatileMetadata(const Metadata* mdata)
QString filename = mdata->Filename();
int rating = mdata->Rating();
int playcount = mdata->PlayCount();
- TagLib::MPEG::File *mpegfile = OpenFile(filename);
- if (!mpegfile)
+ QByteArray fname = filename.toLocal8Bit();
+ if (!OpenFile(fname.constData(), true))
return false;
- TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag();
+ TagLib::ID3v2::Tag *tag = GetID3v2Tag();
if (!tag)
- {
- delete mpegfile;
return false;
- }
bool result = (writeRating(tag, rating) && writePlayCount(tag, playcount));
- mpegfile->save();
- delete mpegfile;
+ if (!SaveFile())
+ return false;
return result;
}
@@ -766,3 +834,20 @@ bool MetaIOID3::writeRating(TagLib::ID3v2::Tag *tag, int rating)
return true;
}
+
+bool MetaIOID3::TagExists(const QString &filename)
+{
+ if (!OpenFile(filename))
+ return false;
+
+ TagLib::ID3v1::Tag *v1_tag = GetID3v1Tag();
+ TagLib::ID3v2::Tag *v2_tag = GetID3v2Tag();
+
+ bool retval = false;
+
+ if ((v2_tag && !v2_tag->isEmpty()) ||
+ (v1_tag && !v1_tag->isEmpty()))
+ retval = true;
+
+ return retval;
+}
View
29 mythplugins/mythmusic/mythmusic/metaioid3.h
@@ -11,8 +11,6 @@
#include <textidentificationframe.h>
#include <attachedpictureframe.h>
#include <popularimeterframe.h>
-#include <mpegproperties.h>
-#include <mpegfile.h>
#include <tfile.h>
// QT
@@ -22,7 +20,6 @@ using TagLib::ID3v2::UserTextIdentificationFrame;
using TagLib::ID3v2::TextIdentificationFrame;
using TagLib::ID3v2::PopularimeterFrame;
using TagLib::ID3v2::AttachedPictureFrame;
-using TagLib::MPEG::Properties;
/*!
* \class MetaIOID3
@@ -39,7 +36,7 @@ class MetaIOID3 : public MetaIOTagLib
MetaIOID3(void);
virtual ~MetaIOID3(void);
- bool write(Metadata* mdata);
+ virtual bool write(Metadata* mdata);
bool writeVolatileMetadata(const Metadata* mdata);
bool writeAlbumArt(const QString &filename, const AlbumArtImage *albumart);
@@ -51,20 +48,36 @@ class MetaIOID3 : public MetaIOTagLib
bool supportsEmbeddedImages(void) { return true; }
- bool changeImageType(const QString &filename, const AlbumArtImage *albumart, ImageType newType);
+ bool changeImageType(const QString &filename, const AlbumArtImage *albumart,
+ ImageType newType);
+
+ virtual bool TagExists(const QString &filename);
private:
- TagLib::MPEG::File *OpenFile(const QString &filename);
+ bool OpenFile(const QString &filename, bool forWriting = false);
+ bool SaveFile();
+ void CloseFile();
+
+ TagLib::ID3v2::Tag* GetID3v2Tag(bool create = false);
+ TagLib::ID3v1::Tag* GetID3v1Tag(bool create = false);
bool writePlayCount(TagLib::ID3v2::Tag *tag, int playcount);
bool writeRating(TagLib::ID3v2::Tag *tag, int rating);
AlbumArtList readAlbumArt(TagLib::ID3v2::Tag *tag);
- UserTextIdentificationFrame* find(TagLib::ID3v2::Tag *tag, const String &description);
+ UserTextIdentificationFrame* find(TagLib::ID3v2::Tag *tag,
+ const String &description);
PopularimeterFrame* findPOPM(TagLib::ID3v2::Tag *tag, const String &email);
- AttachedPictureFrame* findAPIC(TagLib::ID3v2::Tag *tag, const AttachedPictureFrame::Type &type,
+ AttachedPictureFrame* findAPIC(TagLib::ID3v2::Tag *tag,
+ const AttachedPictureFrame::Type &type,
const String &description = String::null);
QString getExtFromMimeType(const QString &mimeType);
+
+ TagLib::File *m_file;
+ QString m_filename;
+
+ typedef enum { kMPEG, kFLAC } TagType;
+ TagType m_fileType;
};
#endif
View
8 mythplugins/mythmusic/mythmusic/metaiooggvorbis.cpp
@@ -134,13 +134,7 @@ Metadata* MetaIOOggVorbis::read(QString filename)
metadata->setCompilation(compilation);
if (metadata->Length() <= 0)
- {
- TagLib::FileRef *fileref = new TagLib::FileRef(oggfile);
- metadata->setLength(getTrackLength(fileref));
- // FileRef takes ownership of oggfile, and is responsible for it's
- // deletion. Messy.
- delete fileref;
- }
+ metadata->setLength(getTrackLength(oggfile));
else
delete oggfile;
View
8 mythplugins/mythmusic/mythmusic/metaiotaglib.cpp
@@ -13,6 +13,7 @@
#include <audioproperties.h>
#include <tag.h>
#include <tstring.h>
+#include <fileref.h>
/* Redefine the TString conversion macros */
#undef QStringToTString
@@ -98,7 +99,7 @@ void MetaIOTagLib::ReadGenericMetadata(Tag *tag, Metadata *metadata)
* \param file Pointer to file object
* \returns An integer (signed!) to represent the length in milliseconds.
*/
-int MetaIOTagLib::getTrackLength(TagLib::FileRef *file)
+int MetaIOTagLib::getTrackLength(TagLib::File *file)
{
int milliseconds = 0;
@@ -119,8 +120,9 @@ int MetaIOTagLib::getTrackLength(QString filename)
int milliseconds = 0;
QByteArray fname = filename.toLocal8Bit();
TagLib::FileRef *file = new TagLib::FileRef(fname.constData());
-
- milliseconds = getTrackLength(file);
+
+ if (file && file->audioProperties())
+ milliseconds = file->audioProperties()->length() * 1000;
// If we didn't get a valid length, add the metadata but show warning.
if (milliseconds <= 1000)
View
5 mythplugins/mythmusic/mythmusic/metaiotaglib.h
@@ -7,7 +7,6 @@
// Taglib
#include <tfile.h>
-#include <fileref.h>
using TagLib::File;
using TagLib::Tag;
@@ -26,9 +25,9 @@ class MetaIOTagLib : public MetaIO
virtual bool write(Metadata* mdata) = 0;
virtual Metadata* read(QString filename) = 0;
-
+
protected:
- int getTrackLength(TagLib::FileRef *file);
+ int getTrackLength(TagLib::File *file);
int getTrackLength(QString filename);
void ReadGenericMetadata(TagLib::Tag *tag, Metadata *metadata);
void WriteGenericMetadata(TagLib::Tag *tag, Metadata *metadata);
View
8 mythplugins/mythmusic/mythmusic/metaiowavpack.cpp
@@ -115,13 +115,7 @@ Metadata* MetaIOWavPack::read(QString filename)
metadata->setCompilation(compilation);
if (metadata->Length() <= 0)
- {
- TagLib::FileRef *fileref = new TagLib::FileRef(wpfile);
- metadata->setLength(getTrackLength(fileref));
- // FileRef takes ownership of wpfile, and is responsible for it's
- // deletion. Messy.
- delete fileref;
- }
+ metadata->setLength(getTrackLength(wpfile));
else
delete wpfile;
Please sign in to comment.
Something went wrong with that request. Please try again.