Changeset View
Changeset View
Standalone View
Standalone View
src/extractors/taglibextractor.cpp
Show All 15 Lines | 1 | /* | |||
---|---|---|---|---|---|
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 "taglibextractor.h" | 21 | #include "taglibextractor.h" | ||
22 | 22 | | |||
23 | // Taglib includes | 23 | // Taglib includes | ||
24 | #include <fileref.h> | 24 | #include <taglib.h> | ||
25 | #include <tag.h> | ||||
25 | #include <tfilestream.h> | 26 | #include <tfilestream.h> | ||
26 | #include <flacfile.h> | 27 | #include <tpropertymap.h> | ||
28 | #include <aifffile.h> | ||||
27 | #include <apefile.h> | 29 | #include <apefile.h> | ||
28 | #include <asftag.h> | 30 | #include <asffile.h> | ||
29 | #include <asfattribute.h> | 31 | #include <flacfile.h> | ||
30 | #include <apetag.h> | 32 | #include <mp4file.h> | ||
31 | #include <mpcfile.h> | 33 | #include <mpcfile.h> | ||
32 | #include <id3v2tag.h> | | |||
33 | #include <id3v1genres.h> | | |||
34 | #include <mpegfile.h> | 34 | #include <mpegfile.h> | ||
35 | #include <oggfile.h> | 35 | #include <oggfile.h> | ||
36 | #include <mp4file.h> | | |||
37 | #include <mp4tag.h> | | |||
38 | #include <taglib.h> | | |||
39 | #include <tag.h> | | |||
40 | #include <vorbisfile.h> | | |||
41 | #include <opusfile.h> | 36 | #include <opusfile.h> | ||
42 | #include <speexfile.h> | 37 | #include <speexfile.h> | ||
43 | #include <xiphcomment.h> | 38 | #include <vorbisfile.h> | ||
44 | #include <popularimeterframe.h> | 39 | #include <wavfile.h> | ||
45 | #include <textidentificationframe.h> | | |||
46 | #include <wavpackfile.h> | 40 | #include <wavpackfile.h> | ||
41 | #include <asftag.h> | ||||
42 | #include <asfattribute.h> | ||||
43 | #include <id3v2tag.h> | ||||
44 | #include <mp4tag.h> | ||||
45 | #include <popularimeterframe.h> | ||||
47 | 46 | | |||
48 | #include <QDateTime> | 47 | #include <QDateTime> | ||
49 | #include <QDebug> | 48 | #include <QDebug> | ||
50 | 49 | | |||
51 | using namespace KFileMetaData; | 50 | using namespace KFileMetaData; | ||
52 | 51 | | |||
53 | TagLibExtractor::TagLibExtractor(QObject* parent) | 52 | TagLibExtractor::TagLibExtractor(QObject* parent) | ||
54 | : ExtractorPlugin(parent) | 53 | : ExtractorPlugin(parent) | ||
Show All 21 Lines | 57 | const QStringList supportedMimeTypes = { | |||
76 | QStringLiteral("audio/x-wavpack"), | 75 | QStringLiteral("audio/x-wavpack"), | ||
77 | }; | 76 | }; | ||
78 | 77 | | |||
79 | QStringList TagLibExtractor::mimetypes() const | 78 | QStringList TagLibExtractor::mimetypes() const | ||
80 | { | 79 | { | ||
81 | return supportedMimeTypes; | 80 | return supportedMimeTypes; | ||
82 | } | 81 | } | ||
83 | 82 | | |||
84 | void TagLibExtractor::extractId3Tags(TagLib::ID3v2::Tag* Id3Tags, ExtractedData& data) | 83 | void extractAudioProperties(TagLib::File* file, ExtractionResult* result) | ||
85 | { | 84 | { | ||
86 | if (Id3Tags->isEmpty()) { | 85 | TagLib::AudioProperties* audioProp = file->audioProperties(); | ||
87 | return; | 86 | if (audioProp) { | ||
smithjd: This could return early if the property map is empty. | |||||
88 | } | 87 | if (audioProp->length()) { | ||
89 | TagLib::ID3v2::FrameList lstID3v2; | 88 | // What about the xml duration? | ||
90 | 89 | result->add(Property::Duration, audioProp->length()); | |||
91 | // Artist. | | |||
92 | lstID3v2 = Id3Tags->frameListMap()["TPE1"]; | | |||
93 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
94 | if (!data.artists.isEmpty()) { | | |||
95 | data.artists += ", "; | | |||
96 | } | | |||
97 | data.artists += frame->toString(); | | |||
98 | } | | |||
99 | | ||||
100 | // Album Artist. | | |||
101 | lstID3v2 = Id3Tags->frameListMap()["TPE2"]; | | |||
102 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
103 | if (!data.albumArtists.isEmpty()) { | | |||
104 | data.albumArtists += ", "; | | |||
105 | } | | |||
106 | data.albumArtists += frame->toString(); | | |||
107 | } | | |||
108 | | ||||
109 | // Composer. | | |||
110 | lstID3v2 = Id3Tags->frameListMap()["TCOM"]; | | |||
111 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
112 | if (!data.composers.isEmpty()) { | | |||
113 | data.composers += ", "; | | |||
114 | } | | |||
115 | data.composers += frame->toString(); | | |||
116 | } | | |||
117 | | ||||
118 | // Lyricist. | | |||
119 | lstID3v2 = Id3Tags->frameListMap()["TEXT"]; | | |||
120 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
121 | if (!data.lyricists.isEmpty()) { | | |||
122 | data.lyricists += ", "; | | |||
123 | } | | |||
124 | data.lyricists += frame->toString(); | | |||
125 | } | | |||
126 | | ||||
127 | // Genre. | | |||
128 | lstID3v2 = Id3Tags->frameListMap()["TCON"]; | | |||
129 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
130 | data.genres.append(frame->toString()); | | |||
131 | } | | |||
132 | | ||||
133 | // Disc number. | | |||
134 | lstID3v2 = Id3Tags->frameListMap()["TPOS"]; | | |||
135 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
136 | data.discNumber = frame->toString().toInt(); | | |||
137 | } | | |||
138 | | ||||
139 | // Performer. | | |||
140 | lstID3v2 = Id3Tags->frameListMap()["TMCL"]; | | |||
141 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
142 | if (!data.performer.isEmpty()) { | | |||
143 | data.performer += ", "; | | |||
144 | } | | |||
145 | data.performer += frame->toString(); | | |||
146 | } | | |||
147 | | ||||
148 | // Conductor. | | |||
149 | lstID3v2 = Id3Tags->frameListMap()["TPE3"]; | | |||
150 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
151 | if (!data.conductor.isEmpty()) { | | |||
152 | data.conductor += ", "; | | |||
153 | } | | |||
154 | data.conductor += frame->toString(); | | |||
155 | } | | |||
156 | | ||||
157 | // Publisher. | | |||
158 | lstID3v2 = Id3Tags->frameListMap()["TPUB"]; | | |||
159 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
160 | if (!data.publisher.isEmpty()) { | | |||
161 | data.publisher += ", "; | | |||
162 | } | | |||
163 | data.publisher += frame->toString(); | | |||
164 | } | | |||
165 | | ||||
166 | // Copyright. | | |||
167 | lstID3v2 = Id3Tags->frameListMap()["TCOP"]; | | |||
168 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
169 | if (!data.copyright.isEmpty()) { | | |||
170 | data.copyright += ", "; | | |||
171 | } | | |||
172 | data.copyright += frame->toString(); | | |||
173 | } | | |||
174 | | ||||
175 | // Language. | | |||
176 | lstID3v2 = Id3Tags->frameListMap()["TLAN"]; | | |||
177 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
178 | if (!data.language.isEmpty()) { | | |||
179 | data.language += ", "; | | |||
180 | } | | |||
181 | data.language += frame->toString(); | | |||
182 | } | | |||
183 | | ||||
184 | // Lyrics. | | |||
185 | lstID3v2 = Id3Tags->frameListMap()["USLT"]; | | |||
186 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
187 | if (!data.lyrics.isEmpty()) { | | |||
188 | data.lyrics += ", "; | | |||
189 | } | | |||
190 | data.lyrics += frame->toString(); | | |||
191 | } | | |||
192 | | ||||
193 | // Compilation. | | |||
194 | lstID3v2 = Id3Tags->frameListMap()["TCMP"]; | | |||
195 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
196 | if (!data.compilation.isEmpty()) { | | |||
197 | data.compilation += ", "; | | |||
198 | } | | |||
199 | data.compilation += frame->toString(); | | |||
200 | } | | |||
201 | | ||||
202 | // Rating. | | |||
203 | /* There is no standard regarding ratings. Most of the implementations match | | |||
204 | a 5 stars rating to a range of 0-255 for MP3. | | |||
205 | Match it to baloo rating with a range of 0 - 10 */ | | |||
206 | lstID3v2 = Id3Tags->frameListMap()["POPM"]; | | |||
207 | for (const auto& frame : qAsConst(lstID3v2)) { | | |||
208 | TagLib::ID3v2::PopularimeterFrame *ratingFrame = static_cast<TagLib::ID3v2::PopularimeterFrame *>(frame); | | |||
209 | int rating = ratingFrame->rating(); | | |||
210 | if (rating == 0) { | | |||
211 | data.rating = 0; | | |||
212 | } else if (rating == 1) { | | |||
213 | TagLib::String ratingProvider = ratingFrame->email(); | | |||
214 | if (ratingProvider == "no@email" || ratingProvider == "org.kde.kfilemetadata") { | | |||
215 | data.rating = 1; | | |||
216 | } else { | | |||
217 | data.rating = 2; | | |||
218 | } | | |||
219 | } else if (rating >= 1 && rating <= 255) { | | |||
220 | data.rating = static_cast<int>(0.032 * rating + 2); | | |||
221 | } | | |||
222 | } | | |||
223 | | ||||
224 | // User Text Frame. | | |||
225 | lstID3v2 = Id3Tags->frameListMap()["TXXX"]; | | |||
226 | if (!lstID3v2.isEmpty()) { | | |||
227 | // look for ReplayGain tags | | |||
228 | typedef TagLib::ID3v2::UserTextIdentificationFrame IdFrame; | | |||
229 | | ||||
230 | auto trackGainFrame = IdFrame::find(Id3Tags, "replaygain_track_gain"); | | |||
231 | if (trackGainFrame && !trackGainFrame->fieldList().isEmpty()) { | | |||
232 | data.replayGainTrackGain = TStringToQString(trackGainFrame->fieldList().back()); | | |||
233 | } | 90 | } | ||
234 | 91 | | |||
235 | auto trackPeakFrame = IdFrame::find(Id3Tags, "replaygain_track_peak"); | 92 | if (audioProp->bitrate()) { | ||
236 | if (trackPeakFrame && !trackPeakFrame->fieldList().isEmpty()) { | 93 | result->add(Property::BitRate, audioProp->bitrate() * 1000); | ||
237 | data.replayGainTrackPeak = TStringToQString(trackPeakFrame->fieldList().back()); | | |||
238 | } | 94 | } | ||
239 | 95 | | |||
240 | auto albumGainFrame = IdFrame::find(Id3Tags, "replaygain_album_gain"); | 96 | if (audioProp->channels()) { | ||
241 | if (albumGainFrame && !albumGainFrame->fieldList().isEmpty()) { | 97 | result->add(Property::Channels, audioProp->channels()); | ||
242 | data.replayGainAlbumGain = TStringToQString(albumGainFrame->fieldList().back()); | | |||
243 | } | 98 | } | ||
244 | 99 | | |||
245 | auto albumPeakFrame = IdFrame::find(Id3Tags, "replaygain_album_peak"); | 100 | if (audioProp->sampleRate()) { | ||
246 | if (albumPeakFrame && !albumPeakFrame->fieldList().isEmpty()) { | 101 | result->add(Property::SampleRate, audioProp->sampleRate()); | ||
247 | data.replayGainAlbumPeak = TStringToQString(albumPeakFrame->fieldList().back()); | | |||
248 | } | 102 | } | ||
249 | } | 103 | } | ||
250 | //TODO handle TIPL tag | | |||
251 | } | 104 | } | ||
252 | 105 | | |||
253 | void TagLibExtractor::extractMp4Tags(TagLib::MP4::Tag* mp4Tags, ExtractedData& data) | 106 | void TagLibExtractor::readGenericProperties(const TagLib::PropertyMap &savedProperties, ExtractionResult* result) | ||
254 | { | 107 | { | ||
255 | if (mp4Tags->isEmpty()) { | 108 | if (savedProperties.isEmpty()) { | ||
256 | return; | 109 | return; | ||
257 | } | 110 | } | ||
258 | TagLib::MP4::ItemListMap allTags = mp4Tags->itemListMap(); | 111 | if (savedProperties.contains("TITLE")) { | ||
259 | 112 | result->add(Property::Title, TStringToQString(savedProperties["TITLE"].toString()).trimmed()); | |||
260 | TagLib::MP4::ItemListMap::Iterator itAlbumArtists = allTags.find("aART"); | | |||
261 | if (itAlbumArtists != allTags.end()) { | | |||
262 | data.albumArtists = itAlbumArtists->second.toStringList().toString(", "); | | |||
263 | } | | |||
264 | | ||||
265 | TagLib::MP4::ItemListMap::Iterator itDiscNumber = allTags.find("disk"); | | |||
266 | if (itDiscNumber != allTags.end()) { | | |||
267 | data.discNumber = itDiscNumber->second.toInt(); | | |||
268 | } | | |||
269 | | ||||
270 | TagLib::MP4::ItemListMap::Iterator itCompilation = allTags.find("cpil"); | | |||
271 | if (itCompilation != allTags.end()) { | | |||
272 | data.compilation = itCompilation->second.toStringList().toString(", "); | | |||
273 | } | | |||
274 | | ||||
275 | TagLib::MP4::ItemListMap::Iterator itCopyright = allTags.find("cprt"); | | |||
276 | if (itCopyright != allTags.end()) { | | |||
277 | data.copyright = itCopyright->second.toStringList().toString(", "); | | |||
278 | } | | |||
279 | | ||||
280 | TagLib::String genreAtomName(TagLib::String("©gen", TagLib::String::UTF8).to8Bit(), TagLib::String::Latin1); | | |||
281 | TagLib::MP4::ItemListMap::Iterator itGenres = allTags.find(genreAtomName); | | |||
282 | if (itGenres != allTags.end()) { | | |||
283 | data.genres = itGenres->second.toStringList().toString(", "); | | |||
284 | } | | |||
285 | | ||||
286 | TagLib::String composerAtomName(TagLib::String("©wrt", TagLib::String::UTF8).to8Bit(), TagLib::String::Latin1); | | |||
287 | TagLib::MP4::ItemListMap::Iterator itComposers = allTags.find(composerAtomName); | | |||
288 | if (itComposers != allTags.end()) { | | |||
289 | data.composers = itComposers->second.toStringList().toString(", "); | | |||
290 | } | | |||
291 | | ||||
292 | /* There is no standard regarding ratings. Mimic MediaMonkey's behavior | | |||
293 | with a range of 0 to 100 (stored in steps of 10) and make it compatible | | |||
294 | with baloo rating with a range from 0 to 10 */ | | |||
295 | TagLib::MP4::ItemListMap::Iterator itRating = allTags.find("rate"); | | |||
296 | if (itRating != allTags.end()) { | | |||
297 | data.rating = itRating->second.toStringList().toString().toInt() / 10; | | |||
298 | } | | |||
299 | | ||||
300 | TagLib::String lyricsAtomName(TagLib::String("©lyr", TagLib::String::UTF8).to8Bit(), TagLib::String::Latin1); | | |||
301 | TagLib::MP4::ItemListMap::Iterator itLyrics = allTags.find(lyricsAtomName); | | |||
302 | if (itLyrics != allTags.end()) { | | |||
303 | data.lyrics = itLyrics->second.toStringList().toString(", "); | | |||
304 | } | 113 | } | ||
114 | if (savedProperties.contains("ALBUM")) { | ||||
115 | result->add(Property::Album, TStringToQString(savedProperties["ALBUM"].toString()).trimmed()); | ||||
305 | } | 116 | } | ||
306 | 117 | if (savedProperties.contains("COMMENT")) { | |||
307 | void TagLibExtractor::extractApeTags(TagLib::APE::Tag* apeTags, ExtractedData& data) | 118 | result->add(Property::Comment, TStringToQString(savedProperties["COMMENT"].toString()).trimmed()); | ||
308 | { | | |||
309 | if (apeTags->isEmpty()) { | | |||
310 | return; | | |||
311 | } | 119 | } | ||
312 | TagLib::APE::ItemListMap lstApe = apeTags->itemListMap(); | 120 | if (savedProperties.contains("TRACKNUMBER")) { | ||
313 | TagLib::APE::ItemListMap::ConstIterator itApe; | 121 | result->add(Property::TrackNumber, savedProperties["TRACKNUMBER"].toString().toInt()); | ||
314 | | ||||
315 | itApe = lstApe.find("ARTIST"); | | |||
316 | if (itApe != lstApe.end()) { | | |||
317 | if (!data.artists.isEmpty()) { | | |||
318 | data.artists += ", "; | | |||
319 | } | 122 | } | ||
320 | data.artists += (*itApe).second.toString(); | 123 | if (savedProperties.contains("DATE")) { | ||
124 | result->add(Property::ReleaseYear, savedProperties["DATE"].toString().toInt()); | ||||
321 | } | 125 | } | ||
322 | 126 | if (savedProperties.contains("OPUS")) { | |||
323 | itApe = lstApe.find("ALBUMARTIST"); | 127 | result->add(Property::Opus, savedProperties["OPUS"].toString().toInt()); | ||
324 | if (itApe == lstApe.end()) { | | |||
325 | itApe = lstApe.find("ALBUM ARTIST"); | | |||
326 | } | 128 | } | ||
327 | if (itApe != lstApe.end()) { | 129 | if (savedProperties.contains("DISCNUMBER")) { | ||
328 | if(!data.albumArtists.isEmpty()) { | 130 | result->add(Property::DiscNumber, savedProperties["DISCNUMBER"].toString().toInt()); | ||
329 | data.albumArtists += ", "; | | |||
330 | } | 131 | } | ||
331 | data.albumArtists += (*itApe).second.toString(); | 132 | if (savedProperties.contains("RATING")) { | ||
133 | result->add(Property::Rating, savedProperties["RATING"].toString().toInt() / 10); | ||||
332 | } | 134 | } | ||
333 | 135 | if (savedProperties.contains("LOCATION")) { | |||
334 | itApe = lstApe.find("COMPOSER"); | 136 | result->add(Property::Location, TStringToQString(savedProperties["LOCATION"].toString()).trimmed()); | ||
335 | if (itApe != lstApe.end()) { | | |||
336 | if (!data.composers.isEmpty()) { | | |||
337 | data.composers += ", "; | | |||
338 | } | 137 | } | ||
339 | data.composers += (*itApe).second.toString(); | 138 | if (savedProperties.contains("LANGUAGE")) { | ||
139 | result->add(Property::Langauge, TStringToQString(savedProperties["LANGUAGE"].toString()).trimmed()); | ||||
340 | } | 140 | } | ||
341 | 141 | if (savedProperties.contains("LICENSE")) { | |||
smithjd: Can this be moved into the asf-specific extractions? | |||||
I wanted to do so first, but that will require to also put the PropertyMap into the extractAsfTags method, which I think is not worth it. astippich: I wanted to do so first, but that will require to also put the PropertyMap into the… | |||||
smithjd: lstASF = asfTags->attribute("WM/Writer");
...
The existing code does this. | |||||
I wanted to avoid the extra querying, but apparently I already did the same for other special cases, so I did as you suggested astippich: I wanted to avoid the extra querying, but apparently I already did the same for other special… | |||||
342 | itApe = lstApe.find("LYRICIST"); | 142 | result->add(Property::License, TStringToQString(savedProperties["LICENSE"].toString()).trimmed()); | ||
343 | if (itApe != lstApe.end()) { | | |||
344 | if (!data.lyricists.isEmpty()) { | | |||
345 | data.lyricists += ", "; | | |||
346 | } | 143 | } | ||
347 | data.lyricists += (*itApe).second.toString(); | 144 | if (savedProperties.contains("PUBLISHER")) { | ||
145 | result->add(Property::Publisher, TStringToQString(savedProperties["PUBLISHER"].toString()).trimmed()); | ||||
348 | } | 146 | } | ||
349 | 147 | if (savedProperties.contains("COPYRIGHT")) { | |||
350 | itApe = lstApe.find("GENRE"); | 148 | result->add(Property::Copyright, TStringToQString(savedProperties["COPYRIGHT"].toString()).trimmed()); | ||
351 | if (itApe != lstApe.end()) { | | |||
352 | data.genres.append((*itApe).second.toString()); | | |||
353 | } | 149 | } | ||
354 | 150 | if (savedProperties.contains("LABEL")) { | |||
355 | itApe = lstApe.find("LOCATION"); | 151 | result->add(Property::Label, TStringToQString(savedProperties["LABEL"].toString()).trimmed()); | ||
356 | if (itApe != lstApe.end()) { | | |||
357 | if (!data.location.isEmpty()) { | | |||
358 | data.location += ", "; | | |||
359 | } | 152 | } | ||
360 | data.location += (*itApe).second.toString(); | 153 | if (savedProperties.contains("ENSEMBLE")) { | ||
154 | result->add(Property::Ensemble, TStringToQString(savedProperties["ENSEMBLE"].toString()).trimmed()); | ||||
361 | } | 155 | } | ||
362 | 156 | if (savedProperties.contains("COMPILATION")) { | |||
363 | itApe = lstApe.find("ARRANGER"); | 157 | result->add(Property::Compilation, TStringToQString(savedProperties["COMPILATION"].toString()).trimmed()); | ||
364 | if (itApe != lstApe.end()) { | | |||
365 | if (!data.arranger.isEmpty()) { | | |||
366 | data.arranger += ", "; | | |||
367 | } | 158 | } | ||
368 | data.arranger += (*itApe).second.toString(); | 159 | if (savedProperties.contains("LYRICS")) { | ||
160 | result->add(Property::Lyrics, TStringToQString(savedProperties["LYRICS"].toString()).trimmed()); | ||||
369 | } | 161 | } | ||
370 | 162 | if (savedProperties.contains("ARTIST")) { | |||
371 | itApe = lstApe.find("PERFORMER"); | 163 | const auto artistString = TStringToQString(savedProperties["ARTIST"].toString(";")).trimmed(); | ||
372 | if (itApe != lstApe.end()) { | 164 | const auto artists = contactsFromString(artistString); | ||
373 | if (!data.performer.isEmpty()) { | 165 | for (auto& artist : artists) { | ||
374 | data.performer += ", "; | 166 | result->add(Property::Artist, artist); | ||
375 | } | 167 | } | ||
376 | data.performer += (*itApe).second.toString(); | | |||
377 | } | 168 | } | ||
378 | 169 | if (savedProperties.contains("GENRE")) { | |||
379 | itApe = lstApe.find("CONDUCTOR"); | 170 | const auto genreString = TStringToQString(savedProperties["GENRE"].toString(";")).trimmed(); | ||
380 | if (itApe != lstApe.end()) { | 171 | const auto genres = contactsFromString(genreString); | ||
381 | if (!data.conductor.isEmpty()) { | 172 | for (auto& genre : genres) { | ||
382 | data.conductor += ", "; | 173 | result->add(Property::Genre, genre); | ||
383 | } | 174 | } | ||
384 | data.conductor += (*itApe).second.toString(); | | |||
385 | } | 175 | } | ||
386 | 176 | if (savedProperties.contains("ALBUMARTIST")) { | |||
387 | itApe = lstApe.find("ENSEMBLE"); | 177 | const auto albumArtistsString = TStringToQString(savedProperties["ALBUMARTIST"].toString(";")).trimmed(); | ||
388 | if (itApe != lstApe.end()) { | 178 | const auto albumArtists = contactsFromString(albumArtistsString); | ||
389 | if (!data.ensemble.isEmpty()) { | 179 | for (auto& res : albumArtists) { | ||
390 | data.ensemble += ", "; | 180 | result->add(Property::AlbumArtist, res); | ||
391 | } | 181 | } | ||
392 | data.ensemble += (*itApe).second.toString(); | | |||
393 | } | 182 | } | ||
394 | 183 | if (savedProperties.contains("COMPOSER")) { | |||
395 | itApe = lstApe.find("PUBLISHER"); | 184 | const auto composersString = TStringToQString(savedProperties["COMPOSER"].toString(";")).trimmed(); | ||
bruns: doubled space | |||||
396 | if (itApe != lstApe.end()) { | 185 | const auto composers = contactsFromString(composersString); | ||
397 | if (!data.publisher.isEmpty()) { | 186 | for (auto& comp : composers) { | ||
398 | data.publisher += ", "; | 187 | result->add(Property::Composer, comp); | ||
399 | } | 188 | } | ||
400 | data.publisher += (*itApe).second.toString(); | | |||
401 | } | 189 | } | ||
402 | 190 | if (savedProperties.contains("LYRICIST")) { | |||
403 | itApe = lstApe.find("COPYRIGHT"); | 191 | const auto lyricistsString = TStringToQString(savedProperties["LYRICIST"].toString(";")).trimmed(); | ||
404 | if (itApe != lstApe.end()) { | 192 | const auto lyricists = contactsFromString(lyricistsString); | ||
405 | if (!data.copyright.isEmpty()) { | 193 | for (auto& lyr : lyricists) { | ||
406 | data.copyright += ", "; | 194 | result->add(Property::Lyricist, lyr); | ||
407 | } | 195 | } | ||
408 | data.copyright += (*itApe).second.toString(); | | |||
409 | } | 196 | } | ||
410 | 197 | /* Lyricist is called "WRITER" for wma/asf files */ | |||
411 | itApe = lstApe.find("LABEL"); | 198 | if (savedProperties.contains("WRITER")) { | ||
412 | if (itApe != lstApe.end()) { | 199 | const auto lyricistsString = TStringToQString(savedProperties["WRITER"].toString(";")).trimmed(); | ||
413 | if (!data.label.isEmpty()) { | 200 | const auto lyricists = contactsFromString(lyricistsString); | ||
414 | data.label += ", "; | 201 | for (auto& lyr : lyricists) { | ||
202 | result->add(Property::Lyricist, lyr); | ||||
415 | } | 203 | } | ||
416 | data.label += (*itApe).second.toString(); | | |||
417 | } | 204 | } | ||
418 | 205 | if (savedProperties.contains("CONDUCTOR")) { | |||
419 | itApe = lstApe.find("AUTHOR"); | 206 | const auto conductorString = TStringToQString(savedProperties["CONDUCTOR"].toString(";")).trimmed(); | ||
420 | if (itApe != lstApe.end()) { | 207 | QStringList conductors = contactsFromString(conductorString); | ||
421 | if (!data.author.isEmpty()) { | 208 | foreach(const QString& arr, conductors) { | ||
bruns: Can you also use the same style as above, i.e. `const auto` and `for( : )`? | |||||
astippich: Oh, damn copy&paste | |||||
422 | data.author += ", "; | 209 | result->add(Property::Conductor, arr); | ||
423 | } | 210 | } | ||
424 | data.author += (*itApe).second.toString(); | | |||
425 | } | 211 | } | ||
426 | 212 | if (savedProperties.contains("ARRANGER")) { | |||
427 | itApe = lstApe.find("LICENSE"); | 213 | const auto arrangerString = TStringToQString(savedProperties["ARRANGER"].toString(";")).trimmed(); | ||
428 | if (itApe != lstApe.end()) { | 214 | QStringList arrangers = contactsFromString(arrangerString); | ||
429 | if (!data.license.isEmpty()) { | 215 | foreach(const QString& arr, arrangers) { | ||
bruns: dito, and below ... | |||||
430 | data.license += ", "; | 216 | result->add(Property::Arranger, arr); | ||
431 | } | 217 | } | ||
432 | data.license += (*itApe).second.toString(); | | |||
433 | } | 218 | } | ||
434 | 219 | if (savedProperties.contains("PERFORMER")) { | |||
435 | itApe = lstApe.find("LYRICS"); | 220 | const auto performersString = TStringToQString(savedProperties["PERFORMER"].toString(";")).trimmed(); | ||
436 | if (itApe != lstApe.end()) { | 221 | QStringList performers = contactsFromString(performersString); | ||
437 | if (!data.lyrics.isEmpty()) { | 222 | foreach(const QString& per, performers) { | ||
438 | data.lyrics += ", "; | 223 | result->add(Property::Performer, per); | ||
439 | } | 224 | } | ||
440 | data.lyrics += (*itApe).second.toString(); | | |||
441 | } | 225 | } | ||
442 | 226 | if (savedProperties.contains("AUTHOR")) { | |||
443 | itApe = lstApe.find("COMPILATION"); | 227 | const auto authorString = TStringToQString(savedProperties["AUTHOR"].toString(";")).trimmed(); | ||
444 | if (itApe != lstApe.end()) { | 228 | QStringList authors = contactsFromString(authorString); | ||
445 | if (!data.compilation.isEmpty()) { | 229 | foreach(const QString& arr, authors) { | ||
446 | data.compilation += ", "; | 230 | result->add(Property::Author, arr); | ||
447 | } | 231 | } | ||
448 | data.compilation += (*itApe).second.toString(); | | |||
449 | } | 232 | } | ||
450 | 233 | | |||
451 | itApe = lstApe.find("LANGUAGE"); | 234 | if (savedProperties.contains("REPLAYGAIN_TRACK_GAIN")) { | ||
452 | if (itApe != lstApe.end()) { | 235 | auto trackGainString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_GAIN"].toString(";")).trimmed(); | ||
453 | if (!data.language.isEmpty()) { | 236 | /* remove " dB" suffix */ | ||
454 | data.language += ", "; | 237 | if (trackGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) { | ||
238 | trackGainString.chop(3); | ||||
455 | } | 239 | } | ||
456 | data.language += (*itApe).second.toString(); | 240 | bool success = false; | ||
241 | double replayGainTrackGain = trackGainString.toDouble(&success); | ||||
242 | if (success) { | ||||
243 | result->add(Property::ReplayGainTrackGain, replayGainTrackGain); | ||||
457 | } | 244 | } | ||
458 | | ||||
459 | | ||||
460 | itApe = lstApe.find("DISC"); | | |||
461 | if (itApe == lstApe.end()) { | | |||
462 | itApe = lstApe.find("DISCNUMBER"); | | |||
463 | } | 245 | } | ||
464 | if (itApe != lstApe.end()) { | 246 | if (savedProperties.contains("REPLAYGAIN_ALBUM_GAIN")) { | ||
465 | data.discNumber = (*itApe).second.toString().toInt(); | 247 | auto albumGainString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_GAIN"].toString(";")).trimmed(); | ||
248 | /* remove " dB" suffix */ | ||||
249 | if (albumGainString.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) { | ||||
250 | albumGainString.chop(3); | ||||
466 | } | 251 | } | ||
467 | 252 | bool success = false; | |||
468 | itApe = lstApe.find("OPUS"); | 253 | double replayGainAlbumGain = albumGainString.toDouble(&success); | ||
469 | if (itApe != lstApe.end()) { | 254 | if (success) { | ||
470 | data.opus = (*itApe).second.toString().toInt(); | 255 | result->add(Property::ReplayGainAlbumGain, replayGainAlbumGain); | ||
471 | } | 256 | } | ||
472 | | ||||
473 | itApe = lstApe.find("RATING"); | | |||
474 | if (itApe != lstApe.end()) { | | |||
475 | /* There is no standard regarding ratings. There is one implementation | | |||
476 | most seem to follow with a range of 0 to 100 (stored in steps of 10). | | |||
477 | Make it compatible with baloo rating with a range from 0 to 10 */ | | |||
478 | data.rating = (*itApe).second.toString().toInt() / 10; | | |||
479 | } | 257 | } | ||
480 | 258 | if (savedProperties.contains("REPLAYGAIN_TRACK_PEAK")) { | |||
481 | itApe = lstApe.find("REPLAYGAIN_TRACK_GAIN"); | 259 | auto trackPeakString = TStringToQString(savedProperties["REPLAYGAIN_TRACK_PEAK"].toString(";")).trimmed(); | ||
482 | if (itApe != lstApe.end()) { | 260 | bool success = false; | ||
483 | data.replayGainTrackGain = TStringToQString((*itApe).second.toString()); | 261 | double replayGainTrackPeak = trackPeakString.toDouble(&success); | ||
262 | if (success) { | ||||
263 | result->add(Property::ReplayGainTrackPeak, replayGainTrackPeak); | ||||
484 | } | 264 | } | ||
485 | | ||||
486 | itApe = lstApe.find("REPLAYGAIN_TRACK_PEAK"); | | |||
487 | if (itApe != lstApe.end()) { | | |||
488 | data.replayGainTrackPeak = TStringToQString((*itApe).second.toString()); | | |||
489 | } | 265 | } | ||
490 | 266 | if (savedProperties.contains("REPLAYGAIN_ALBUM_PEAK")) { | |||
491 | itApe = lstApe.find("REPLAYGAIN_ALBUM_GAIN"); | 267 | auto albumPeakString = TStringToQString(savedProperties["REPLAYGAIN_ALBUM_PEAK"].toString(";")).trimmed(); | ||
492 | if (itApe != lstApe.end()) { | 268 | bool success = false; | ||
493 | data.replayGainAlbumGain = TStringToQString((*itApe).second.toString()); | 269 | double replayGainAlbumPeak = albumPeakString.toDouble(&success); | ||
270 | if (success) { | ||||
271 | result->add(Property::ReplayGainAlbumPeak, replayGainAlbumPeak); | ||||
494 | } | 272 | } | ||
495 | | ||||
496 | itApe = lstApe.find("REPLAYGAIN_ALBUM_PEAK"); | | |||
497 | if (itApe != lstApe.end()) { | | |||
498 | data.replayGainAlbumPeak = TStringToQString((*itApe).second.toString()); | | |||
499 | } | 273 | } | ||
500 | } | 274 | } | ||
501 | 275 | | |||
502 | void TagLibExtractor::extractVorbisTags(TagLib::Ogg::XiphComment* vorbisTags, ExtractedData& data) | 276 | void TagLibExtractor::extractId3Tags(TagLib::ID3v2::Tag* Id3Tags, ExtractionResult* result) | ||
503 | { | 277 | { | ||
504 | if (vorbisTags->isEmpty()) { | 278 | if (Id3Tags->isEmpty()) { | ||
505 | return; | 279 | return; | ||
506 | } | 280 | } | ||
507 | TagLib::Ogg::FieldListMap lstOgg = vorbisTags->fieldListMap(); | 281 | TagLib::ID3v2::FrameList lstID3v2; | ||
508 | TagLib::Ogg::FieldListMap::ConstIterator itOgg; | | |||
509 | | ||||
510 | itOgg = lstOgg.find("ARTIST"); | | |||
511 | if (itOgg != lstOgg.end()) { | | |||
512 | if (!data.artists.isEmpty()) { | | |||
513 | data.artists += ", "; | | |||
514 | } | | |||
515 | data.artists += (*itOgg).second.toString(", "); | | |||
516 | } | | |||
517 | | ||||
518 | itOgg = lstOgg.find("ALBUMARTIST"); | | |||
519 | if (itOgg == lstOgg.end()) { | | |||
520 | itOgg = lstOgg.find("ALBUM ARTIST"); | | |||
521 | } | | |||
522 | if (itOgg != lstOgg.end()) { | | |||
523 | if (!data.albumArtists.isEmpty()) { | | |||
524 | data.albumArtists += ", "; | | |||
525 | } | | |||
526 | data.albumArtists += (*itOgg).second.toString(", "); | | |||
527 | } | | |||
528 | | ||||
529 | itOgg = lstOgg.find("COMPOSER"); | | |||
530 | if (itOgg != lstOgg.end()) { | | |||
531 | if (!data.composers.isEmpty()) { | | |||
532 | data.composers += ", "; | | |||
533 | } | | |||
534 | data.composers += (*itOgg).second.toString(", "); | | |||
535 | } | | |||
536 | | ||||
537 | itOgg = lstOgg.find("LYRICIST"); | | |||
538 | if (itOgg != lstOgg.end()) { | | |||
539 | if (!data.lyricists.isEmpty()) { | | |||
540 | data.lyricists += ", "; | | |||
541 | } | | |||
542 | data.lyricists += (*itOgg).second.toString(", "); | | |||
543 | } | | |||
544 | | ||||
545 | itOgg = lstOgg.find("LOCATION"); | | |||
546 | if (itOgg != lstOgg.end()) { | | |||
547 | if (!data.location.isEmpty()) { | | |||
548 | data.location += ", "; | | |||
549 | } | | |||
550 | data.location += (*itOgg).second.toString(", "); | | |||
551 | } | | |||
552 | | ||||
553 | itOgg = lstOgg.find("ARRANGER"); | | |||
554 | if (itOgg != lstOgg.end()) { | | |||
555 | if (!data.arranger.isEmpty()) { | | |||
556 | data.arranger += ", "; | | |||
557 | } | | |||
558 | data.arranger += (*itOgg).second.toString(", "); | | |||
559 | } | | |||
560 | | ||||
561 | itOgg = lstOgg.find("PERFORMER"); | | |||
562 | if (itOgg != lstOgg.end()) { | | |||
563 | if (!data.performer.isEmpty()) { | | |||
564 | data.performer += ", "; | | |||
565 | } | | |||
566 | data.performer += (*itOgg).second.toString(", "); | | |||
567 | } | | |||
568 | | ||||
569 | itOgg = lstOgg.find("CONDUCTOR"); | | |||
570 | if (itOgg != lstOgg.end()) { | | |||
571 | if (!data.conductor.isEmpty()) { | | |||
572 | data.conductor += ", "; | | |||
573 | } | | |||
574 | data.conductor += (*itOgg).second.toString(", "); | | |||
575 | } | | |||
576 | | ||||
577 | itOgg = lstOgg.find("ENSEMBLE"); | | |||
578 | if (itOgg != lstOgg.end()) { | | |||
579 | if (!data.ensemble.isEmpty()) { | | |||
580 | data.ensemble += ", "; | | |||
581 | } | | |||
582 | data.ensemble += (*itOgg).second.toString(", "); | | |||
583 | } | | |||
584 | | ||||
585 | itOgg = lstOgg.find("PUBLISHER"); | | |||
586 | if (itOgg != lstOgg.end()) { | | |||
587 | if (!data.publisher.isEmpty()) { | | |||
588 | data.publisher += ", "; | | |||
589 | } | | |||
590 | data.publisher += (*itOgg).second.toString(", "); | | |||
591 | } | | |||
592 | | ||||
593 | itOgg = lstOgg.find("COPYRIGHT"); | | |||
594 | if (itOgg != lstOgg.end()) { | | |||
595 | if (!data.copyright.isEmpty()) { | | |||
596 | data.copyright += ", "; | | |||
597 | } | | |||
598 | data.copyright += (*itOgg).second.toString(", "); | | |||
599 | } | | |||
600 | | ||||
601 | itOgg = lstOgg.find("LABEL"); | | |||
602 | if (itOgg != lstOgg.end()) { | | |||
603 | if (!data.label.isEmpty()) { | | |||
604 | data.label += ", "; | | |||
605 | } | | |||
606 | data.label += (*itOgg).second.toString(", "); | | |||
607 | } | | |||
608 | | ||||
609 | itOgg = lstOgg.find("AUTHOR"); | | |||
610 | if (itOgg != lstOgg.end()) { | | |||
611 | if (!data.author.isEmpty()) { | | |||
612 | data.author += ", "; | | |||
613 | } | | |||
614 | data.author += (*itOgg).second.toString(", "); | | |||
615 | } | | |||
616 | | ||||
617 | itOgg = lstOgg.find("LICENSE"); | | |||
618 | if (itOgg != lstOgg.end()) { | | |||
619 | if (!data.license.isEmpty()) { | | |||
620 | data.license += ", "; | | |||
621 | } | | |||
622 | data.license += (*itOgg).second.toString(", "); | | |||
623 | } | | |||
624 | | ||||
625 | itOgg = lstOgg.find("LYRICS"); | | |||
626 | if (itOgg != lstOgg.end()) { | | |||
627 | if (!data.lyrics.isEmpty()) { | | |||
628 | data.lyrics += ", "; | | |||
629 | } | | |||
630 | data.lyrics += (*itOgg).second.toString(", "); | | |||
631 | } | | |||
632 | | ||||
633 | itOgg = lstOgg.find("COMPILATION"); | | |||
634 | if (itOgg != lstOgg.end()) { | | |||
635 | if (!data.compilation.isEmpty()) { | | |||
636 | data.compilation += ", "; | | |||
637 | } | | |||
638 | data.compilation += (*itOgg).second.toString(", "); | | |||
639 | } | | |||
640 | | ||||
641 | itOgg = lstOgg.find("LANGUAGE"); | | |||
642 | if (itOgg != lstOgg.end()) { | | |||
643 | if (!data.language.isEmpty()) { | | |||
644 | data.language += ", "; | | |||
645 | } | | |||
646 | data.language += (*itOgg).second.toString(", "); | | |||
647 | } | | |||
648 | 282 | | |||
649 | itOgg = lstOgg.find("GENRE"); | 283 | // Publisher. | ||
650 | if (itOgg != lstOgg.end()) { | 284 | /* Special handling because TagLib::PropertyMap matches "TPUB" to "LABEL" | ||
651 | data.genres.append((*itOgg).second); | 285 | * Insert manually for Publisher */ | ||
bruns: `*/` on a separate line | |||||
286 | lstID3v2 = Id3Tags->frameListMap()["TPUB"]; | ||||
287 | if (!lstID3v2.isEmpty()) { | ||||
288 | result->add(Property::Publisher, TStringToQString(lstID3v2.front()->toString())); | ||||
652 | } | 289 | } | ||
653 | 290 | | |||
654 | itOgg = lstOgg.find("DISCNUMBER"); | 291 | // Compilation. | ||
655 | if (itOgg != lstOgg.end()) { | 292 | lstID3v2 = Id3Tags->frameListMap()["TCMP"]; | ||
656 | data.discNumber = (*itOgg).second.toString("").toInt(); | 293 | if (!lstID3v2.isEmpty()) { | ||
294 | result->add(Property::Compilation, TStringToQString(lstID3v2.front()->toString())); | ||||
657 | } | 295 | } | ||
658 | 296 | | |||
659 | itOgg = lstOgg.find("OPUS"); | 297 | // Rating. | ||
660 | if (itOgg != lstOgg.end()) { | 298 | /* There is no standard regarding ratings. Most of the implementations match | ||
661 | data.opus = (*itOgg).second.toString("").toInt(); | 299 | a 5 stars rating to a range of 0-255 for MP3. | ||
300 | Match it to baloo rating with a range of 0 - 10 */ | ||||
bruns: `*/` on separate line, leading `*` on other lines | |||||
bruns: s/Match/Map/ | |||||
301 | lstID3v2 = Id3Tags->frameListMap()["POPM"]; | ||||
302 | if (!lstID3v2.isEmpty()) { | ||||
303 | TagLib::ID3v2::PopularimeterFrame *ratingFrame = static_cast<TagLib::ID3v2::PopularimeterFrame *>(lstID3v2.front()); | ||||
304 | int rating = ratingFrame->rating(); | ||||
305 | if (rating == 0) { | ||||
306 | rating = 0; | ||||
307 | } else if (rating == 1) { | ||||
308 | TagLib::String ratingProvider = ratingFrame->email(); | ||||
309 | if (ratingProvider == "no@email" || ratingProvider == "org.kde.kfilemetadata") { | ||||
310 | rating = 1; | ||||
311 | } else { | ||||
312 | rating = 2; | ||||
662 | } | 313 | } | ||
663 | 314 | } else if (rating >= 1 && rating <= 255) { | |||
664 | itOgg = lstOgg.find("RATING"); | 315 | rating = static_cast<int>(0.032 * rating + 2); | ||
665 | if (itOgg != lstOgg.end()) { | | |||
666 | //there is no standard regarding ratings. There is one implementation | | |||
667 | //most seem to follow with a range of 0 to 100 (stored in steps of 10). | | |||
668 | //make it compatible with baloo rating with a range from 0 to 10 | | |||
669 | data.rating = (*itOgg).second.toString("").toInt() / 10; | | |||
670 | } | 316 | } | ||
671 | 317 | result->add(Property::Rating, rating); | |||
672 | itOgg = lstOgg.find("REPLAYGAIN_TRACK_GAIN"); | | |||
673 | if (itOgg != lstOgg.end()) { | | |||
674 | data.replayGainTrackGain = TStringToQString((*itOgg).second.toString("")); | | |||
675 | } | 318 | } | ||
676 | | ||||
677 | itOgg = lstOgg.find("REPLAYGAIN_TRACK_PEAK"); | | |||
678 | if (itOgg != lstOgg.end()) { | | |||
679 | data.replayGainTrackPeak = TStringToQString((*itOgg).second.toString("")); | | |||
680 | } | 319 | } | ||
681 | 320 | | |||
682 | itOgg = lstOgg.find("REPLAYGAIN_ALBUM_GAIN"); | 321 | void TagLibExtractor::extractMp4Tags(TagLib::MP4::Tag* mp4Tags, ExtractionResult* result) | ||
683 | if (itOgg != lstOgg.end()) { | 322 | { | ||
684 | data.replayGainAlbumGain = TStringToQString((*itOgg).second.toString("")); | 323 | if (mp4Tags->isEmpty()) { | ||
324 | return; | ||||
685 | } | 325 | } | ||
326 | TagLib::MP4::ItemListMap allTags = mp4Tags->itemListMap(); | ||||
686 | 327 | | |||
687 | itOgg = lstOgg.find("REPLAYGAIN_ALBUM_PEAK"); | 328 | /* There is no standard regarding ratings. Mimic MediaMonkey's behavior | ||
688 | if (itOgg != lstOgg.end()) { | 329 | with a range of 0 to 100 (stored in steps of 10) and make it compatible | ||
689 | data.replayGainAlbumPeak = TStringToQString((*itOgg).second.toString("")); | 330 | with baloo rating with a range from 0 to 10 */ | ||
bruns: dito | |||||
331 | TagLib::MP4::ItemListMap::Iterator itRating = allTags.find("rate"); | ||||
332 | if (itRating != allTags.end()) { | ||||
333 | result->add(Property::Rating, itRating->second.toStringList().toString().toInt() / 10); | ||||
690 | } | 334 | } | ||
691 | } | 335 | } | ||
692 | 336 | | |||
693 | void TagLibExtractor::extractAsfTags(TagLib::ASF::Tag* asfTags, ExtractedData& data) | 337 | void TagLibExtractor::extractAsfTags(TagLib::ASF::Tag* asfTags, ExtractionResult* result) | ||
694 | { | 338 | { | ||
695 | if (asfTags->isEmpty()) { | 339 | if (asfTags->isEmpty()) { | ||
696 | return; | 340 | return; | ||
697 | } | 341 | } | ||
698 | 342 | | |||
699 | if (!asfTags->copyright().isEmpty()) { | | |||
700 | data.copyright = asfTags->copyright(); | | |||
701 | } | | |||
702 | | ||||
703 | TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/SharedUserRating"); | 343 | TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/SharedUserRating"); | ||
704 | if (!lstASF.isEmpty()) { | 344 | if (!lstASF.isEmpty()) { | ||
705 | int rating = lstASF.front().toString().toInt(); | 345 | int rating = lstASF.front().toString().toInt(); | ||
706 | //map the rating values of WMP to Baloo rating | 346 | //map the rating values of WMP to Baloo rating | ||
707 | //0->0, 1->2, 25->4, 50->6, 75->8, 99->10 | 347 | //0->0, 1->2, 25->4, 50->6, 75->8, 99->10 | ||
708 | if (rating == 0) { | 348 | if (rating == 0) { | ||
709 | data.rating = 0; | 349 | rating = 0; | ||
710 | } else if (rating == 1) { | 350 | } else if (rating == 1) { | ||
711 | data.rating = 2; | 351 | rating = 2; | ||
712 | } else { | 352 | } else { | ||
713 | data.rating = static_cast<uint>(0.09 * rating + 2); | 353 | rating = static_cast<uint>(0.09 * rating + 2); | ||
714 | } | 354 | } | ||
355 | result->add(Property::Rating, rating); | ||||
715 | } | 356 | } | ||
716 | 357 | | |||
717 | lstASF = asfTags->attribute("WM/PartOfSet"); | 358 | lstASF = asfTags->attribute("Author"); | ||
718 | if (!lstASF.isEmpty()) { | 359 | if (!lstASF.isEmpty()) { | ||
719 | data.discNumber = lstASF.front().toString().toInt(); | 360 | const auto attribute = lstASF.front(); | ||
This intermediate list is not required, you can directly call result->add() for each attribute in lstASF. bruns: This intermediate list is not required, you can directly call `result->add()` for each… | |||||
This intermediate list is not required, you can directly call result->add() for each attribute in lstASF. bruns: This intermediate list is not required, you can directly call result->add() for each attribute… | |||||
After further investigation there is no need to look for more than one entry, since there cannot be duplicated keys. This is also what TagLib does internally. So I changed it to be more inline with the generic extraction. astippich: After further investigation there is no need to look for more than one entry, since there… | |||||
720 | } | 361 | result->add(Property::Author, TStringToQString(attribute.toString()).trimmed()); | ||
721 | | ||||
722 | lstASF = asfTags->attribute("WM/AlbumArtist"); | | |||
723 | for (const auto& attribute : qAsConst(lstASF)) { | | |||
724 | if (!data.albumArtists.isEmpty()) { | | |||
725 | data.albumArtists += ", "; | | |||
726 | } | | |||
727 | data.albumArtists += attribute.toString(); | | |||
728 | } | | |||
729 | | ||||
730 | lstASF = asfTags->attribute("WM/Composer"); | | |||
731 | for (const auto& attribute : qAsConst(lstASF)) { | | |||
732 | if (!data.composers.isEmpty()) { | | |||
733 | data.composers += ", "; | | |||
734 | } | | |||
735 | data.composers += attribute.toString(); | | |||
736 | } | | |||
737 | | ||||
738 | lstASF = asfTags->attribute("WM/Conductor"); | | |||
739 | for (const auto& attribute : qAsConst(lstASF)) { | | |||
740 | if (!data.conductor.isEmpty()) { | | |||
741 | data.conductor += ", "; | | |||
742 | } | | |||
743 | data.conductor += attribute.toString(); | | |||
744 | } | | |||
745 | | ||||
746 | lstASF = asfTags->attribute("WM/Writer"); | | |||
747 | for (const auto& attribute : qAsConst(lstASF)) { | | |||
748 | if (!data.lyricists.isEmpty()) { | | |||
749 | data.lyricists += ", "; | | |||
750 | } | | |||
751 | data.lyricists += attribute.toString(); | | |||
752 | } | 362 | } | ||
753 | 363 | | |||
364 | /* TagLib exports "WM/PUBLISHER in the PropertyMap, | ||||
365 | * add it manually to Publisher */ | ||||
754 | lstASF = asfTags->attribute("WM/Publisher"); | 366 | lstASF = asfTags->attribute("WM/Publisher"); | ||
755 | for (const auto& attribute : qAsConst(lstASF)) { | 367 | if (!lstASF.isEmpty()) { | ||
756 | if (!data.publisher.isEmpty()) { | 368 | const auto attribute = lstASF.front(); | ||
757 | data.publisher += ", "; | 369 | result->add(Property::Publisher, TStringToQString(attribute.toString()).trimmed()); | ||
758 | } | | |||
759 | data.publisher += attribute.toString(); | | |||
760 | } | | |||
761 | | ||||
762 | lstASF = asfTags->attribute("Author"); | | |||
763 | for (const auto& attribute : qAsConst(lstASF)) { | | |||
764 | if (!data.author.isEmpty()) { | | |||
765 | data.author += ", "; | | |||
766 | } | | |||
767 | data.author += attribute.toString(); | | |||
768 | } | 370 | } | ||
769 | } | 371 | } | ||
770 | 372 | | |||
771 | void TagLibExtractor::extract(ExtractionResult* result) | 373 | void TagLibExtractor::extract(ExtractionResult* result) | ||
bruns: Why only the first element? | |||||
772 | { | 374 | { | ||
773 | const QString fileUrl = result->inputUrl(); | 375 | const QString fileUrl = result->inputUrl(); | ||
774 | const QString mimeType = result->inputMimetype(); | 376 | const QString mimeType = result->inputMimetype(); | ||
775 | 377 | | |||
776 | // Open the file readonly. Important if we're sandboxed. | 378 | // Open the file readonly. Important if we're sandboxed. | ||
777 | TagLib::FileStream stream(fileUrl.toUtf8().constData(), true); | 379 | TagLib::FileStream stream(fileUrl.toUtf8().constData(), true); | ||
778 | if (!stream.isOpen()) { | 380 | if (!stream.isOpen()) { | ||
779 | qWarning() << "Unable to open file readonly: " << fileUrl; | 381 | qWarning() << "Unable to open file readonly: " << fileUrl; | ||
780 | return; | 382 | return; | ||
781 | } | 383 | } | ||
782 | 384 | | |||
783 | TagLib::FileRef file(&stream, true); | | |||
784 | if (file.isNull()) { | | |||
785 | qWarning() << "Unable to open file: " << fileUrl; | | |||
786 | return; | | |||
787 | } | | |||
788 | | ||||
789 | TagLib::Tag* tags = file.tag(); | | |||
790 | result->addType(Type::Audio); | | |||
791 | | ||||
792 | ExtractedData data; | | |||
793 | | ||||
794 | if ((mimeType == QLatin1String("audio/mpeg")) || (mimeType == QLatin1String("audio/mpeg3")) | 385 | if ((mimeType == QLatin1String("audio/mpeg")) || (mimeType == QLatin1String("audio/mpeg3")) | ||
795 | || (mimeType == QLatin1String("audio/x-mpeg"))) { | 386 | || (mimeType == QLatin1String("audio/x-mpeg"))) { | ||
796 | TagLib::MPEG::File mpegFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true); | 387 | TagLib::MPEG::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true); | ||
797 | if (mpegFile.hasID3v2Tag()) { | 388 | extractAudioProperties(&file, result); | ||
798 | extractId3Tags(mpegFile.ID3v2Tag(), data); | 389 | readGenericProperties(file.properties(), result); | ||
799 | } | 390 | if (file.hasID3v2Tag()) { | ||
800 | } else if ((mimeType == QLatin1String("audio/x-aiff")) || (mimeType == QLatin1String("audio/wav")) | 391 | extractId3Tags(file.ID3v2Tag(), result); | ||
801 | || (mimeType == QLatin1String("audio/x-wav"))) { | 392 | } | ||
802 | /* For some reason, TagLib::RIFF::AIFF::File and TagLib::RIFF::WAV::File tag() return | 393 | } else if (mimeType == QLatin1String("audio/x-aiff")) { | ||
803 | * only an invalid pointer. Use the dynamic_cast instead. */ | 394 | TagLib::RIFF::AIFF::File file(&stream, true); | ||
804 | TagLib::ID3v2::Tag* ID3v2Tag = dynamic_cast<TagLib::ID3v2::Tag*>(tags); | 395 | extractAudioProperties(&file, result); | ||
805 | if (ID3v2Tag) { | 396 | readGenericProperties(file.properties(), result); | ||
806 | extractId3Tags(ID3v2Tag, data); | 397 | if (file.hasID3v2Tag()) { | ||
807 | } | 398 | extractId3Tags(file.tag(), result); | ||
808 | } else if (mimeType == QLatin1String("audio/mp4")) { | 399 | } | ||
809 | TagLib::MP4::File mp4File(&stream, true); | 400 | } else if ((mimeType == QLatin1String("audio/wav")) || (mimeType == QLatin1String("audio/x-wav"))) { | ||
810 | if (mp4File.hasMP4Tag()) { | 401 | TagLib::RIFF::WAV::File file(&stream, true); | ||
811 | extractMp4Tags(mp4File.tag(), data); | 402 | extractAudioProperties(&file, result); | ||
403 | readGenericProperties(file.properties(), result); | ||||
404 | if (file.hasID3v2Tag()) { | ||||
405 | extractId3Tags(file.tag(), result); | ||||
812 | } | 406 | } | ||
813 | } else if (mimeType == QLatin1String("audio/x-musepack")) { | 407 | } else if (mimeType == QLatin1String("audio/x-musepack")) { | ||
814 | TagLib::MPC::File mpcFile(&stream, true); | 408 | TagLib::MPC::File file(&stream, true); | ||
815 | if (mpcFile.hasAPETag()) { | 409 | extractAudioProperties(&file, result); | ||
816 | extractApeTags(mpcFile.APETag(), data); | 410 | readGenericProperties(file.properties(), result); | ||
817 | } | | |||
818 | } else if (mimeType == QLatin1String("audio/x-ape")) { | 411 | } else if (mimeType == QLatin1String("audio/x-ape")) { | ||
819 | TagLib::APE::File apeFile(&stream, true); | 412 | TagLib::APE::File file(&stream, true); | ||
820 | if (apeFile.hasAPETag()) { | 413 | extractAudioProperties(&file, result); | ||
821 | extractApeTags(apeFile.APETag(), data); | 414 | readGenericProperties(file.properties(), result); | ||
822 | } | | |||
823 | } else if (mimeType == QLatin1String("audio/x-wavpack")) { | 415 | } else if (mimeType == QLatin1String("audio/x-wavpack")) { | ||
824 | TagLib::WavPack::File wavpackFile(&stream, true); | 416 | TagLib::WavPack::File file(&stream, true); | ||
825 | if (wavpackFile.hasAPETag()) { | 417 | extractAudioProperties(&file, result); | ||
826 | extractApeTags(wavpackFile.APETag(), data); | 418 | readGenericProperties(file.properties(), result); | ||
827 | } | 419 | } else if (mimeType == QLatin1String("audio/mp4")) { | ||
420 | TagLib::MP4::File file(&stream, true); | ||||
421 | extractAudioProperties(&file, result); | ||||
422 | readGenericProperties(file.properties(), result); | ||||
423 | extractMp4Tags(file.tag(), result); | ||||
828 | } else if (mimeType == QLatin1String("audio/flac")) { | 424 | } else if (mimeType == QLatin1String("audio/flac")) { | ||
829 | TagLib::FLAC::File flacFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true); | 425 | TagLib::FLAC::File file(&stream, TagLib::ID3v2::FrameFactory::instance(), true); | ||
830 | if (flacFile.hasXiphComment()) { | 426 | extractAudioProperties(&file, result); | ||
831 | extractVorbisTags(flacFile.xiphComment(), data); | 427 | readGenericProperties(file.properties(), result); | ||
832 | } | | |||
833 | } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) { | 428 | } else if (mimeType == QLatin1String("audio/ogg") || mimeType == QLatin1String("audio/x-vorbis+ogg")) { | ||
834 | TagLib::Ogg::Vorbis::File oggFile(&stream, true); | 429 | TagLib::Ogg::Vorbis::File file(&stream, true); | ||
835 | if (oggFile.tag()) { | 430 | extractAudioProperties(&file, result); | ||
836 | extractVorbisTags(oggFile.tag(), data); | 431 | readGenericProperties(file.properties(), result); | ||
837 | } | | |||
838 | } else if (mimeType == QLatin1String("audio/opus") || mimeType == QLatin1String("audio/x-opus+ogg")) { | 432 | } else if (mimeType == QLatin1String("audio/opus") || mimeType == QLatin1String("audio/x-opus+ogg")) { | ||
839 | TagLib::Ogg::Opus::File opusFile(&stream, true); | 433 | TagLib::Ogg::Opus::File file(&stream, true); | ||
840 | if (opusFile.tag()) { | 434 | extractAudioProperties(&file, result); | ||
841 | extractVorbisTags(opusFile.tag(), data); | 435 | readGenericProperties(file.properties(), result); | ||
842 | } | | |||
843 | } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex")) { | 436 | } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex")) { | ||
844 | TagLib::Ogg::Speex::File speexFile(&stream, true); | 437 | TagLib::Ogg::Speex::File file(&stream, true); | ||
845 | if (speexFile.tag()) { | 438 | extractAudioProperties(&file, result); | ||
846 | extractVorbisTags(speexFile.tag(), data); | 439 | readGenericProperties(file.properties(), result); | ||
847 | } | | |||
848 | } else if (mimeType == QLatin1String("audio/x-ms-wma")) { | 440 | } else if (mimeType == QLatin1String("audio/x-ms-wma")) { | ||
849 | /* For some reason, TagLib::ASF::File tag() returns only an invalid pointer. | 441 | TagLib::ASF::File file(&stream, true); | ||
850 | * Use the dynamic_cast instead. */ | 442 | extractAudioProperties(&file, result); | ||
851 | TagLib::ASF::Tag* asfTags = dynamic_cast<TagLib::ASF::Tag*>(tags); | 443 | readGenericProperties(file.properties(), result); | ||
852 | if (asfTags) { | 444 | extractAsfTags(file.tag(), result); | ||
853 | extractAsfTags(asfTags, data); | | |||
854 | } | | |||
855 | } | | |||
856 | | ||||
857 | if (!tags->isEmpty()) { | | |||
858 | QString title = TStringToQString(tags->title()); | | |||
859 | if (!title.isEmpty()) { | | |||
860 | result->add(Property::Title, title); | | |||
861 | } | | |||
862 | | ||||
863 | QString comment = TStringToQString(tags->comment()); | | |||
864 | if (!comment.isEmpty()) { | | |||
865 | result->add(Property::Comment, comment); | | |||
866 | } | | |||
867 | | ||||
868 | if (data.genres.isEmpty()) { | | |||
869 | data.genres.append(tags->genre()); | | |||
870 | } | | |||
871 | | ||||
872 | for (uint i = 0; i < data.genres.size(); i++) { | | |||
873 | QString genre = TStringToQString(data.genres[i]).trimmed(); | | |||
874 | if (!genre.isEmpty()) { | | |||
875 | // Convert from int | | |||
876 | bool ok = false; | | |||
877 | int genreNum = genre.toInt(&ok); | | |||
878 | if (ok) { | | |||
879 | genre = TStringToQString(TagLib::ID3v1::genre(genreNum)); | | |||
880 | } | | |||
881 | result->add(Property::Genre, genre); | | |||
882 | } | | |||
883 | } | | |||
884 | | ||||
885 | const auto artistString = data.artists.isEmpty() | | |||
886 | ? TStringToQString(tags->artist()) | | |||
887 | : TStringToQString(data.artists).trimmed(); | | |||
888 | const auto artists = contactsFromString(artistString); | | |||
889 | for (auto& artist : artists) { | | |||
890 | result->add(Property::Artist, artist); | | |||
891 | } | | |||
892 | | ||||
893 | const auto composersString = TStringToQString(data.composers).trimmed(); | | |||
894 | const auto composers = contactsFromString(composersString); | | |||
895 | for (auto& comp : composers) { | | |||
896 | result->add(Property::Composer, comp); | | |||
897 | } | 445 | } | ||
898 | 446 | | |||
899 | const auto lyricistsString = TStringToQString(data.lyricists).trimmed(); | 447 | result->addType(Type::Audio); | ||
900 | const auto lyricists = contactsFromString(lyricistsString); | | |||
901 | for (auto& lyr : lyricists) { | | |||
902 | result->add(Property::Lyricist, lyr); | | |||
903 | } | | |||
904 | | ||||
905 | const auto album = TStringToQString(tags->album()); | | |||
906 | if (!album.isEmpty()) { | | |||
907 | result->add(Property::Album, album); | | |||
908 | | ||||
909 | const auto albumArtistsString = TStringToQString(data.albumArtists).trimmed(); | | |||
910 | const auto albumArtists = contactsFromString(albumArtistsString); | | |||
911 | for (auto& res : albumArtists) { | | |||
912 | result->add(Property::AlbumArtist, res); | | |||
913 | } | | |||
914 | } | | |||
915 | | ||||
916 | if (tags->track()) { | | |||
917 | result->add(Property::TrackNumber, tags->track()); | | |||
918 | } | | |||
919 | | ||||
920 | if (tags->year()) { | | |||
921 | result->add(Property::ReleaseYear, tags->year()); | | |||
922 | } | | |||
923 | | ||||
924 | QString locationsString = TStringToQString(data.location).trimmed(); | | |||
925 | QStringList locations = contactsFromString(locationsString); | | |||
926 | foreach(const QString& loc, locations) { | | |||
927 | result->add(Property::Location, loc); | | |||
928 | } | | |||
929 | | ||||
930 | QString performersString = TStringToQString(data.performer).trimmed(); | | |||
931 | QStringList performers = contactsFromString(performersString); | | |||
932 | foreach(const QString& per, performers) { | | |||
933 | result->add(Property::Performer, per); | | |||
934 | } | | |||
935 | | ||||
936 | QString ensembleString = TStringToQString(data.ensemble).trimmed(); | | |||
937 | QStringList ensembles = contactsFromString(ensembleString); | | |||
938 | foreach(const QString& ens, ensembles) { | | |||
939 | result->add(Property::Ensemble, ens); | | |||
940 | } | | |||
941 | | ||||
942 | QString arrangerString = TStringToQString(data.arranger).trimmed(); | | |||
943 | QStringList arrangers = contactsFromString(arrangerString); | | |||
944 | foreach(const QString& arr, arrangers) { | | |||
945 | result->add(Property::Arranger, arr); | | |||
946 | } | | |||
947 | | ||||
948 | QString conductorString = TStringToQString(data.conductor).trimmed(); | | |||
949 | QStringList conductors = contactsFromString(conductorString); | | |||
950 | foreach(const QString& arr, conductors) { | | |||
951 | result->add(Property::Conductor, arr); | | |||
952 | } | | |||
953 | | ||||
954 | QString publisherString = TStringToQString(data.publisher).trimmed(); | | |||
955 | QStringList publishers = contactsFromString(publisherString); | | |||
956 | foreach(const QString& arr, publishers) { | | |||
957 | result->add(Property::Publisher, arr); | | |||
958 | } | | |||
959 | | ||||
960 | QString copyrightString = TStringToQString(data.copyright).trimmed(); | | |||
961 | QStringList copyrights = contactsFromString(copyrightString); | | |||
962 | foreach(const QString& arr, copyrights) { | | |||
963 | result->add(Property::Copyright, arr); | | |||
964 | } | | |||
965 | | ||||
966 | QString labelString = TStringToQString(data.label).trimmed(); | | |||
967 | QStringList labels = contactsFromString(labelString); | | |||
968 | foreach(const QString& arr, labels) { | | |||
969 | result->add(Property::Label, arr); | | |||
970 | } | | |||
971 | | ||||
972 | QString authorString = TStringToQString(data.author).trimmed(); | | |||
973 | QStringList authors = contactsFromString(authorString); | | |||
974 | foreach(const QString& arr, authors) { | | |||
975 | result->add(Property::Author, arr); | | |||
976 | } | | |||
977 | | ||||
978 | QString languageString = TStringToQString(data.language).trimmed(); | | |||
979 | QStringList languages = contactsFromString(languageString); | | |||
980 | foreach(const QString& arr, languages) { | | |||
981 | result->add(Property::Language, arr); | | |||
982 | } | | |||
983 | | ||||
984 | QString licenseString = TStringToQString(data.license).trimmed(); | | |||
985 | QStringList licenses = contactsFromString(licenseString); | | |||
986 | foreach(const QString& arr, licenses) { | | |||
987 | result->add(Property::License, arr); | | |||
988 | } | | |||
989 | | ||||
990 | QString compilationString = TStringToQString(data.compilation).trimmed(); | | |||
991 | QStringList compilations = contactsFromString(compilationString); | | |||
992 | foreach(const QString& arr, compilations) { | | |||
993 | result->add(Property::Compilation, arr); | | |||
994 | } | | |||
995 | | ||||
996 | QString lyricsString = TStringToQString(data.lyrics).trimmed(); | | |||
997 | if (!lyricsString.isEmpty()) { | | |||
998 | result->add(Property::Lyrics, lyricsString); | | |||
999 | } | | |||
1000 | | ||||
1001 | if (data.opus.isValid()) { | | |||
1002 | result->add(Property::Opus, data.opus); | | |||
1003 | } | | |||
1004 | | ||||
1005 | if (data.discNumber.isValid()) { | | |||
1006 | result->add(Property::DiscNumber, data.discNumber); | | |||
1007 | } | | |||
1008 | | ||||
1009 | if (data.rating.isValid()) { | | |||
1010 | result->add(Property::Rating, data.rating); | | |||
1011 | } | | |||
1012 | | ||||
1013 | if (!data.replayGainAlbumGain.isEmpty()) { | | |||
1014 | /* remove " dB" suffix */ | | |||
1015 | if (data.replayGainAlbumGain.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) | | |||
1016 | { | | |||
1017 | data.replayGainAlbumGain.chop(3); | | |||
1018 | } | | |||
1019 | bool success = false; | | |||
1020 | double replayGainAlbumGain = data.replayGainAlbumGain.toDouble(&success); | | |||
1021 | if (success) { | | |||
1022 | result->add(Property::ReplayGainAlbumGain, replayGainAlbumGain); | | |||
1023 | } | | |||
1024 | } | | |||
1025 | | ||||
1026 | if (!data.replayGainAlbumPeak.isEmpty()) { | | |||
1027 | bool success = false; | | |||
1028 | double replayGainAlbumPeak = data.replayGainAlbumPeak.toDouble(&success); | | |||
1029 | if (success) { | | |||
1030 | result->add(Property::ReplayGainAlbumPeak, replayGainAlbumPeak); | | |||
1031 | } | | |||
1032 | } | | |||
1033 | | ||||
1034 | if (!data.replayGainTrackGain.isEmpty()) { | | |||
1035 | /* remove " dB" suffix */ | | |||
1036 | if (data.replayGainTrackGain.endsWith(QStringLiteral(" dB"), Qt::CaseInsensitive)) | | |||
1037 | { | | |||
1038 | data.replayGainTrackGain.chop(3); | | |||
1039 | } | | |||
1040 | bool success = false; | | |||
1041 | double replayGainTrackGain = data.replayGainTrackGain.toDouble(&success); | | |||
1042 | if (success) { | | |||
1043 | result->add(Property::ReplayGainTrackGain, replayGainTrackGain); | | |||
1044 | } | | |||
1045 | } | | |||
1046 | | ||||
1047 | if (!data.replayGainTrackPeak.isEmpty()) { | | |||
1048 | bool success = false; | | |||
1049 | double replayGainTrackPeak = data.replayGainTrackPeak.toDouble(&success); | | |||
1050 | if (success) { | | |||
1051 | result->add(Property::ReplayGainTrackPeak, replayGainTrackPeak); | | |||
1052 | } | | |||
1053 | } | | |||
1054 | } | | |||
1055 | | ||||
1056 | TagLib::AudioProperties* audioProp = file.audioProperties(); | | |||
1057 | if (audioProp) { | | |||
1058 | if (audioProp->length()) { | | |||
1059 | // What about the xml duration? | | |||
1060 | result->add(Property::Duration, audioProp->length()); | | |||
1061 | } | | |||
1062 | | ||||
1063 | if (audioProp->bitrate()) { | | |||
1064 | result->add(Property::BitRate, audioProp->bitrate() * 1000); | | |||
1065 | } | | |||
1066 | | ||||
1067 | if (audioProp->channels()) { | | |||
1068 | result->add(Property::Channels, audioProp->channels()); | | |||
1069 | } | | |||
1070 | | ||||
1071 | if (audioProp->sampleRate()) { | | |||
1072 | result->add(Property::SampleRate, audioProp->sampleRate()); | | |||
1073 | } | | |||
1074 | } | | |||
1075 | } | 448 | } | ||
1076 | 449 | | |||
1077 | // TAG information (incomplete). | 450 | // TAG information (incomplete). | ||
1078 | // https://xiph.org/vorbis/doc/v-comment.html | 451 | // https://xiph.org/vorbis/doc/v-comment.html | ||
1079 | // https://help.mp3tag.de/main_tags.html | 452 | // https://help.mp3tag.de/main_tags.html | ||
1080 | // http://id3.org/ | 453 | // http://id3.org/ | ||
1081 | // https://www.legroom.net/2009/05/09/ogg-vorbis-and-flac-comment-field-recommendations | 454 | // https://www.legroom.net/2009/05/09/ogg-vorbis-and-flac-comment-field-recommendations | ||
1082 | // https://kodi.wiki/view/Music_tagging#Tags_Kodi_reads | 455 | // https://kodi.wiki/view/Music_tagging#Tags_Kodi_reads | ||
Show All 22 Lines |
This could return early if the property map is empty.