Changeset View
Standalone View
src/writers/taglibwriter.cpp
Show All 15 Lines | |||||
16 | * License along with this library; if not, write to the Free Software | 16 | * License along with this library; if not, write to the Free Software | ||
17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
18 | * | 18 | * | ||
19 | */ | 19 | */ | ||
20 | 20 | | |||
21 | #include "taglibwriter.h" | 21 | #include "taglibwriter.h" | ||
22 | 22 | | |||
23 | #include <taglib.h> | 23 | #include <taglib.h> | ||
24 | #include <tfilestream.h> | ||||
25 | #include <tpropertymap.h> | ||||
24 | #include <tstring.h> | 26 | #include <tstring.h> | ||
25 | #include <id3v2tag.h> | 27 | #include <aifffile.h> | ||
26 | #include <fileref.h> | 28 | #include <apefile.h> | ||
29 | #include <asffile.h> | ||||
30 | #include <flacfile.h> | ||||
31 | #include <mp4file.h> | ||||
32 | #include <mpcfile.h> | ||||
33 | #include <mpegfile.h> | ||||
34 | #include <oggfile.h> | ||||
35 | #include <opusfile.h> | ||||
36 | #include <vorbisfile.h> | ||||
37 | #include <speexfile.h> | ||||
38 | #include <wavpackfile.h> | ||||
39 | #include <wavfile.h> | ||||
27 | 40 | | |||
28 | using namespace KFileMetaData; | 41 | #include <QDebug> | ||
29 | 42 | | |||
30 | TagLibWriter::TagLibWriter(QObject* parent) | 43 | namespace { | ||
31 | : WriterPlugin(parent) | | |||
32 | { | | |||
33 | } | | |||
34 | 44 | | |||
35 | const QStringList supportedMimeTypes = { | 45 | const QStringList supportedMimeTypes = { | ||
36 | QStringLiteral("audio/flac"), | 46 | QStringLiteral("audio/flac"), | ||
37 | QStringLiteral("audio/mp4"), | 47 | QStringLiteral("audio/mp4"), | ||
38 | QStringLiteral("audio/mpeg"), | 48 | QStringLiteral("audio/mpeg"), | ||
39 | QStringLiteral("audio/mpeg3"), | 49 | QStringLiteral("audio/mpeg3"), | ||
40 | QStringLiteral("audio/ogg"), | 50 | QStringLiteral("audio/ogg"), | ||
41 | QStringLiteral("audio/opus"), | 51 | QStringLiteral("audio/opus"), | ||
42 | QStringLiteral("audio/speex"), | 52 | QStringLiteral("audio/speex"), | ||
43 | QStringLiteral("audio/wav"), | 53 | QStringLiteral("audio/wav"), | ||
44 | QStringLiteral("audio/x-aiff"), | 54 | QStringLiteral("audio/x-aiff"), | ||
45 | QStringLiteral("audio/x-ape"), | 55 | QStringLiteral("audio/x-ape"), | ||
46 | QStringLiteral("audio/x-mpeg"), | 56 | QStringLiteral("audio/x-mpeg"), | ||
47 | QStringLiteral("audio/x-ms-wma"), | 57 | QStringLiteral("audio/x-ms-wma"), | ||
48 | QStringLiteral("audio/x-musepack"), | 58 | QStringLiteral("audio/x-musepack"), | ||
49 | QStringLiteral("audio/x-opus+ogg"), | 59 | QStringLiteral("audio/x-opus+ogg"), | ||
50 | QStringLiteral("audio/x-speex"), | 60 | QStringLiteral("audio/x-speex+ogg"), | ||
51 | QStringLiteral("audio/x-vorbis+ogg"), | 61 | QStringLiteral("audio/x-vorbis+ogg"), | ||
52 | QStringLiteral("audio/x-wav"), | 62 | QStringLiteral("audio/x-wav"), | ||
53 | QStringLiteral("audio/x-wavpack"), | 63 | QStringLiteral("audio/x-wavpack"), | ||
54 | }; | 64 | }; | ||
55 | 65 | | |||
56 | QStringList TagLibWriter::writeMimetypes() const | | |||
57 | { | | |||
58 | return supportedMimeTypes; | | |||
59 | } | | |||
60 | | ||||
61 | void TagLibWriter::write(const WriteData& data) | | |||
62 | { | | |||
63 | const QString fileUrl = data.inputUrl(); | | |||
64 | const PropertyMap properties = data.getAllProperties(); | | |||
65 | | ||||
66 | TagLib::FileRef file(fileUrl.toUtf8().constData(), true); | | |||
67 | if (file.isNull()) { | | |||
68 | return; | | |||
69 | } | 66 | } | ||
70 | 67 | | |||
71 | TagLib::Tag* tags = file.tag(); | 68 | using namespace KFileMetaData; | ||
bruns: use a smart pointer (unique_ptr, QScopedPtr) here, and get rid of the delete below. | |||||
72 | | ||||
73 | TagLib::String title; | | |||
74 | TagLib::String artist; | | |||
75 | TagLib::String album; | | |||
76 | TagLib::String genre; | | |||
77 | TagLib::String comment; | | |||
78 | 69 | | |||
79 | if (properties.contains(Property::Title)) { | 70 | void writeGenericProperties(TagLib::PropertyMap &oldProperties, const PropertyMap &newProperties) | ||
80 | title = QStringToTString(properties.value(Property::Title).toString()); | 71 | { | ||
81 | tags->setTitle(title); | 72 | if (newProperties.contains(Property::Title)) { | ||
73 | oldProperties.replace("TITLE", QStringToTString(newProperties.value(Property::Title).toString())); | ||||
When you make this dito for the specializations in D18604, just pass in Taglib::File*, and call auto tags = dynamic_cast<FooTag*>(file->tag()); there. bruns: When you make this
`void writeGenericProperties(Taglib::File *file, const PropertyMap… | |||||
That will then require to load and write the property map twice when properties only specific to some tagging formats need to be written, see e.g. Ape and Vorbis tags in D18604. I would like to avoid this. astippich: That will then require to load and write the property map twice when properties only specific… | |||||
So iff the rating is updated at the same time as another property, taglib has to adjust some in-memory structure twice. Obviously, for most formats this poses no problem, but for Ape/Vorbis it does? Also, this no longer applies when you use the specific tag types for Ape/Ogg. Writing to disk only happens when calling file->save()... bruns: So **iff** the rating is updated at the same time as another property, taglib has to adjust… | |||||
Not sure I understand. I would like to avoid doing file->properties(); and file->setProperties(); twice for those formats for which only the PropertyMap is sufficient, e.g. Ape and Ogg, to avoid doing any unnecessary work. The beauty of Ape and Ogg in the TagLib implementation is that they solely work with the PropertyMap and do not require any special handling. See D18826, Ape and Ogg do not have any extra codepath at all. For writing tags, we have to be a little bit more careful, though. If writing Rating information is added to writeGenericProperties, for example Id3v2's "TXXX" tags will be polluted with these values. astippich: Not sure I understand. I would like to avoid doing `file->properties();` and `file… | |||||
I think for reading it as generic as possible is fine, but for writing being a little bit more explicit does not hurt. My proposal for Ape and Ogg is to split the writing for these to two different functions. Yes, it is possible to use the propertymap for both, as both use the same scale and the same tag name, but the implementations on the taglib side are completely different. Writing the "rating" for XiphComment (Ogg) and Ape in distinct functions (see my comment in D18604) hardly adds any code, but gets the Ape and Ogg code paths in line with the other file formats. You don't write the ASF/MP4 rating using the property interface although it would be possible, and IMHO thats the right thing to do, also for Ape and Ogg. As soon Ape and Ogg are split, you no longer rely on the PropertyMap for the rating, and you won't have to use properties()/setProperties() twice. bruns: I think for **reading** it as generic as possible is fine, but for writing being a little bit… | |||||
No, I cannot use the the PropertyMap for ASF/MP4, those atoms/attributes are unsupported in the PropertyMap and need to be handled separately. I would have done so if it is possible. astippich: No, I cannot use the the PropertyMap for ASF/MP4, those atoms/attributes are unsupported in the… | |||||
Taglib does not "handle this nicely". For APE and Xiph, it just accepts *any* unknown key and uses it verbatim, while for MP4 and ASF it rejects any unknown key. The setProperties() is also quite inconsistent, for APE and ASF it only removes items which have an empty value, while for Xiph, the properties are completely replaced. As soon as you add support for a property where APE and Xiph key naming differs, or is only supported by one, you will require two functions anyway. Using APE::Tag::setItem(...) is as efficient as manipulating the key/value in the Taglib::PropertyMap first and setting it by setProperties(...). Likewise for Xiph. If you want to squeeze out the last bit of efficiency, you would skip the setProperties(...) completely when no property is changed by writeGenericProperties(....). This happens e.g. if you only change the rating. If you want to avoid duplicate code, move the properties()/setProperties() into writeGenericProperties(), that saves 12*2 lines and adds 2. bruns: Taglib does not "handle this nicely". For APE and Xiph, it just accepts *any* unknown key and… | |||||
And so do APE::Tag::setItem and OGG::XiphComment::addField ...? btw, also perfectly legal, APE and Xiph allow writing arbitrary tags, while the others do not.
Xiph explicitly allows multiple entries per key, which need to be removed when writing.
TagLib automatically translates different keys from APE to "common names", e.g. DISC->DISCNUMBER etc. I would really like to hand off manual tag handling to TagLib as much as possible. The library solely responsible for reading tags usually knows better how to handle the tags than we do (with a few exceptions to the rule of course). astippich: >Taglib does not "handle this nicely". For APE and Xiph, it just accepts *any* unknown key and… | |||||
By using the type specific function you signal you are aware of the differences between the two, and supply the appropriate data. RATING is not handled by the properties interface, it just works by coincidence, not by design. In case Taglib properly handles a tag, I am not against using it, as said several times. This is the case for DISC, but not for RATING. bruns: By using the type specific function you signal you are aware of the differences between the two… | |||||
82 | } | 74 | } | ||
83 | 75 | | |||
84 | if (properties.contains(Property::Artist)) { | 76 | if (newProperties.contains(Property::Artist)) { | ||
85 | artist = QStringToTString(properties.value(Property::Artist).toString()); | 77 | oldProperties.replace("ARTIST", QStringToTString(newProperties.value(Property::Artist).toString())); | ||
86 | tags->setArtist(artist); | | |||
87 | } | 78 | } | ||
88 | 79 | | |||
89 | if (properties.contains(Property::Album)) { | 80 | if (newProperties.contains(Property::Album)) { | ||
90 | album = QStringToTString(properties.value(Property::Album).toString()); | 81 | oldProperties.replace("ALBUM", QStringToTString(newProperties.value(Property::Album).toString())); | ||
91 | tags->setAlbum(album); | | |||
92 | } | 82 | } | ||
93 | 83 | | |||
94 | if (properties.contains(Property::TrackNumber)) { | 84 | if (newProperties.contains(Property::TrackNumber)) { | ||
95 | int trackNumber = properties.value(Property::TrackNumber).toInt(); | 85 | int trackNumber = newProperties.value(Property::TrackNumber).toInt(); | ||
96 | //taglib requires uint | 86 | //taglib requires uint | ||
97 | if (trackNumber >= 0) { | 87 | if (trackNumber >= 0) { | ||
98 | tags->setTrack(trackNumber); | 88 | oldProperties.replace("TRACKNUMBER", QStringToTString(newProperties.value(Property::TrackNumber).toString())); | ||
99 | } | 89 | } | ||
100 | } | 90 | } | ||
101 | 91 | | |||
102 | if (properties.contains(Property::ReleaseYear)) { | 92 | if (newProperties.contains(Property::ReleaseYear)) { | ||
103 | int year = properties.value(Property::ReleaseYear).toInt(); | 93 | int year = newProperties.value(Property::ReleaseYear).toInt(); | ||
104 | //taglib requires uint | 94 | //taglib requires uint | ||
105 | if (year >= 0) { | 95 | if (year >= 0) { | ||
106 | tags->setYear(year); | 96 | oldProperties.replace("DATE", QStringToTString(newProperties.value(Property::ReleaseYear).toString())); | ||
107 | } | 97 | } | ||
108 | } | 98 | } | ||
109 | 99 | | |||
110 | if (properties.contains(Property::Genre)) { | 100 | if (newProperties.contains(Property::Genre)) { | ||
111 | genre = QStringToTString(properties.value(Property::Genre).toString()); | 101 | oldProperties.replace("GENRE", QStringToTString(newProperties.value(Property::Genre).toString())); | ||
112 | tags->setGenre(genre); | | |||
113 | } | 102 | } | ||
114 | 103 | | |||
115 | if (properties.contains(Property::Comment)) { | 104 | if (newProperties.contains(Property::Comment)) { | ||
116 | comment = QStringToTString(properties.value(Property::Comment).toString()); | 105 | oldProperties.replace("COMMENT", QStringToTString(newProperties.value(Property::Comment).toString())); | ||
117 | tags->setComment(comment); | 106 | } | ||
118 | } | 107 | } | ||
119 | 108 | | |||
109 | TagLibWriter::TagLibWriter(QObject* parent) | ||||
110 | : WriterPlugin(parent) | ||||
111 | { | ||||
112 | } | ||||
120 | 113 | | |||
114 | QStringList TagLibWriter::writeMimetypes() const | ||||
115 | { | ||||
116 | return supportedMimeTypes; | ||||
117 | } | ||||
118 | | ||||
119 | void TagLibWriter::write(const WriteData& data) | ||||
120 | { | ||||
121 | const QString fileUrl = data.inputUrl(); | ||||
122 | const PropertyMap properties = data.getAllProperties(); | ||||
123 | const QString mimeType = data.inputMimetype(); | ||||
124 | | ||||
125 | TagLib::FileStream stream(fileUrl.toUtf8().constData(), false); | ||||
126 | if (!stream.isOpen()) { | ||||
127 | qWarning() << "Unable to open file in write mode: " << fileUrl; | ||||
128 | return; | ||||
129 | } | ||||
130 | | ||||
131 | if ((mimeType == QLatin1String("audio/mpeg")) || (mimeType == QLatin1String("audio/mpeg3")) | ||||
132 | || (mimeType == QLatin1String("audio/x-mpeg"))) { | ||||
133 | TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), false); | ||||
134 | if (file.isValid()) { | ||||
135 | auto savedProperties = file.properties(); | ||||
136 | writeGenericProperties(savedProperties, properties); | ||||
137 | file.setProperties(savedProperties); | ||||
138 | file.save(); | ||||
139 | } | ||||
140 | } else if (mimeType == QLatin1String("audio/x-aiff")) { | ||||
141 | TagLib::RIFF::AIFF::File file(&stream, false); | ||||
142 | if (file.isValid()) { | ||||
143 | auto savedProperties = file.properties(); | ||||
144 | writeGenericProperties(savedProperties, properties); | ||||
145 | file.setProperties(savedProperties); | ||||
146 | file.save(); | ||||
147 | } | ||||
148 | } else if ((mimeType == QLatin1String("audio/wav")) || (mimeType == QLatin1String("audio/x-wav"))) { | ||||
149 | TagLib::RIFF::WAV::File file(&stream, false); | ||||
150 | if (file.isValid()) { | ||||
151 | auto savedProperties = file.properties(); | ||||
152 | writeGenericProperties(savedProperties, properties); | ||||
153 | file.setProperties(savedProperties); | ||||
154 | file.save(); | ||||
155 | } | ||||
156 | } else if (mimeType == QLatin1String("audio/x-musepack")) { | ||||
157 | TagLib::MPC::File file(&stream, false); | ||||
158 | if (file.isValid()) { | ||||
159 | auto savedProperties = file.properties(); | ||||
160 | writeGenericProperties(savedProperties, properties); | ||||
161 | file.setProperties(savedProperties); | ||||
162 | file.save(); | ||||
163 | } | ||||
164 | } else if (mimeType == QLatin1String("audio/x-ape")) { | ||||
165 | TagLib::APE::File file(&stream, false); | ||||
166 | if (file.isValid()) { | ||||
167 | auto savedProperties = file.properties(); | ||||
168 | writeGenericProperties(savedProperties, properties); | ||||
169 | file.setProperties(savedProperties); | ||||
170 | file.save(); | ||||
171 | } | ||||
172 | } else if (mimeType == QLatin1String("audio/x-wavpack")) { | ||||
173 | TagLib::WavPack::File file(&stream, false); | ||||
174 | if (file.isValid()) { | ||||
175 | auto savedProperties = file.properties(); | ||||
176 | writeGenericProperties(savedProperties, properties); | ||||
177 | file.setProperties(savedProperties); | ||||
178 | file.save(); | ||||
179 | } | ||||
180 | } else if (mimeType == QLatin1String("audio/mp4")) { | ||||
181 | TagLib::MP4::File file(&stream, false); | ||||
182 | if (file.isValid()) { | ||||
183 | auto savedProperties = file.properties(); | ||||
184 | writeGenericProperties(savedProperties, properties); | ||||
185 | file.setProperties(savedProperties); | ||||
186 | file.save(); | ||||
187 | } | ||||
188 | } else if (mimeType == QLatin1String("audio/flac")) { | ||||
189 | TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), false); | ||||
190 | if (file.isValid()) { | ||||
191 | auto savedProperties = file.properties(); | ||||
192 | writeGenericProperties(savedProperties, properties); | ||||
193 | file.setProperties(savedProperties); | ||||
194 | file.save(); | ||||
195 | } | ||||
196 | } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) { | ||||
197 | TagLib::Ogg::Vorbis::File file(&stream, false); | ||||
198 | if (file.isValid()) { | ||||
199 | auto savedProperties = file.properties(); | ||||
200 | writeGenericProperties(savedProperties, properties); | ||||
201 | file.setProperties(savedProperties); | ||||
121 | file.save(); | 202 | file.save(); | ||
122 | } | 203 | } | ||
204 | } else if (mimeType == QLatin1String("audio/opus") || mimeType == QLatin1String("audio/x-opus+ogg")) { | ||||
205 | TagLib::Ogg::Opus::File file(&stream, false); | ||||
206 | if (file.isValid()) { | ||||
207 | auto savedProperties = file.properties(); | ||||
208 | writeGenericProperties(savedProperties, properties); | ||||
209 | file.setProperties(savedProperties); | ||||
210 | file.save(); | ||||
211 | } | ||||
212 | } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex+ogg")) { | ||||
213 | TagLib::Ogg::Speex::File file(&stream, false); | ||||
214 | if (file.isValid()) { | ||||
215 | auto savedProperties = file.properties(); | ||||
216 | writeGenericProperties(savedProperties, properties); | ||||
217 | file.setProperties(savedProperties); | ||||
218 | file.save(); | ||||
219 | } | ||||
220 | } else if (mimeType == QLatin1String("audio/x-ms-wma")) { | ||||
221 | TagLib::ASF::File file(&stream, false); | ||||
222 | if (file.isValid()) { | ||||
223 | auto savedProperties = file.properties(); | ||||
224 | writeGenericProperties(savedProperties, properties); | ||||
225 | file.setProperties(savedProperties); | ||||
226 | file.save(); | ||||
227 | } | ||||
228 | } | ||||
229 | } |
use a smart pointer (unique_ptr, QScopedPtr) here, and get rid of the delete below.