GCC Code Coverage Report


Directory: ./
File: mediadetails.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 100 109 91.7%
Functions: 6 6 100.0%
Branches: 133 246 54.1%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include <taglib/audioproperties.h>
16 #include <taglib/fileref.h>
17 #include <taglib/tag.h>
18 #include <taglib/tstring.h>
19 #include <sys/stat.h>
20 #include <stdio.h>
21 #include <string.h>
22 #include <string>
23
24 #include "common.h"
25 #include "log.h"
26 #include "settings.h"
27 #include "commands/Commands.h"
28
29 #include "mediadetails.h"
30
31 MediaDetails MediaDetails::INSTANCE;
32
33 4326 MediaDetails::MediaDetails() :
34 4326 year(0), track(0), length(0), seconds(0), minutes(0), lengthMS(0), bitrate(0), sampleRate(0), channels(0) {
35 4326 }
36 2438 MediaDetails::~MediaDetails() {
37 2438 }
38
39 2132 void MediaDetails::Clear() {
40 2132 title.clear();
41 2132 artist.clear();
42 2132 album.clear();
43 2132 year = 0;
44 2132 comment.clear();
45 2132 track = 0;
46 2132 genre.clear();
47
48 2132 length = 0;
49 2132 lengthMS = 0;
50 2132 seconds = 0;
51 2132 minutes = 0;
52
53 2132 bitrate = 0;
54 2132 sampleRate = 0;
55 2132 channels = 0;
56 2132 }
57
58 // TagLib only reads a file's header, and some files carry no length there: a
59 // fragmented MP4 (an empty moov followed by moof/mdat fragments) declares a
60 // duration of zero, and finding the real one means walking every fragment --
61 // seconds of work on a single-core board, far too slow to do as a track
62 // starts. The web UI has already done it: it runs ffprobe on every media file
63 // and caches the result in config/media_durations.cache, keyed by the name
64 // relative to the music or video directory and checked against the file size.
65 // Take that whenever it is longer than what TagLib found.
66 2111 static void UseCachedProbeDuration(MediaDetails& details, const std::string& fullPath) {
67 2111 std::string name;
68
7/18
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2111 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2111 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2111 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2111 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2111 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2180 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
12735 for (const std::string& dir : { FPP_DIR_MUSIC("/"), FPP_DIR_VIDEO("/") }) {
69
3/4
✓ Branch 0 taken 2180 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2111 times.
✓ Branch 3 taken 69 times.
2180 if (startsWith(fullPath, dir)) {
70
1/2
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
2111 name = fullPath.substr(dir.size());
71 2111 break;
72 }
73
2/4
✓ Branch 0 taken 4222 times.
✓ Branch 1 taken 2111 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
6333 }
74
3/6
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2111 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2111 times.
✗ Branch 5 not taken.
2111 std::string cacheFile = FPP_DIR_CONFIG("/media_durations.cache");
75 struct stat st;
76
7/10
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2111 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 2105 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 6 times.
✓ Branch 8 taken 2105 times.
✓ Branch 9 taken 6 times.
2111 if (name.empty() || !FileExists(cacheFile) || stat(fullPath.c_str(), &st) != 0) {
77 2105 return;
78 }
79
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 Json::Value cache;
80
9/14
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✓ Branch 11 taken 4 times.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 4 times.
6 if (!LoadJsonFromFile(cacheFile, cache) || !cache.isObject() || !cache.isMember(name)) {
81 2 return;
82 }
83 // PHP writes the size as a number and ffprobe's duration as a string, but
84 // accept either for both.
85 8 auto number = [](const Json::Value& v) {
86
8/14
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✓ Branch 11 taken 4 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
8 return v.isString() ? atof(v.asString().c_str()) : (v.isNumeric() ? v.asDouble() : 0.0);
87 };
88
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 const Json::Value& entry = cache[name];
89
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
4 if ((off_t)number(entry["filesize"]) != st.st_size) {
90 ✗ return;
91 }
92
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 int ms = (int)(number(entry["duration"]) * 1000.0);
93
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (ms <= details.lengthMS) {
94 ✗ return;
95 }
96
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogDebug(VB_MEDIAOUT, " Length %d ms from the media duration cache (TagLib found %d ms)\n", ms, details.lengthMS);
97 4 details.lengthMS = ms;
98 4 details.length = ms / 1000;
99 4 details.seconds = details.length % 60;
100 4 details.minutes = details.length / 60;
101
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2107 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 2107 times.
4220 }
102
103 2113 void MediaDetails::ParseMedia(const char* mediaFilename) {
104 char fullMediaPath[2048];
105
106 // Cleared before any early return: a failed parse must not leave the
107 // previous file's details (or, for a fresh object, garbage) behind.
108
1/2
✓ Branch 0 taken 2113 times.
✗ Branch 1 not taken.
2113 Clear();
109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2113 times.
2113 if (!mediaFilename)
110 ✗ return;
111
112
1/2
✓ Branch 0 taken 2113 times.
✗ Branch 1 not taken.
2113 LogDebug(VB_MEDIAOUT, "ParseMedia(%s)\n", mediaFilename);
113
114
6/8
✓ Branch 0 taken 1986 times.
✓ Branch 1 taken 127 times.
✓ Branch 2 taken 1986 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1986 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1986 times.
✓ Branch 7 taken 127 times.
2113 if ((mediaFilename[0] == '/') && FileExists(mediaFilename)) {
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1986 times.
1986 if (strlen(mediaFilename) >= sizeof(fullMediaPath)) {
116 ✗ LogErr(VB_MEDIAOUT, "Unable to parse media details for %s, path name too long\n",
117 mediaFilename);
118 ✗ return;
119 }
120 1986 snprintf(fullMediaPath, sizeof(fullMediaPath), "%s", mediaFilename);
121 } else {
122
5/10
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 127 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 127 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 127 times.
254 if (snprintf(fullMediaPath, 2048, "%s", FPP_DIR_MUSIC("/" + mediaFilename).c_str()) >= 2048) {
123 ✗ LogErr(VB_MEDIAOUT, "Unable to parse media details for %s, full path name too long\n",
124 mediaFilename);
125 ✗ return;
126 }
127
128
3/4
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 119 times.
127 if (!FileExists(fullMediaPath)) {
129
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 8 times.
16 if (snprintf(fullMediaPath, 2048, "%s", FPP_DIR_VIDEO("/" + mediaFilename).c_str()) >= 2048) {
130 ✗ LogErr(VB_MEDIAOUT, "Unable to parse media details for %s, full path name too long\n",
131 mediaFilename);
132 ✗ return;
133 }
134
135
3/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
8 if (!FileExists(fullMediaPath)) {
136
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_MEDIAOUT, "Unable to find %s media file to parse meta data\n", mediaFilename);
137 2 return;
138 }
139 }
140 }
141
142
1/2
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
2111 TagLib::FileRef f(fullMediaPath);
143
144
7/10
✓ Branch 0 taken 2111 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✓ Branch 3 taken 71 times.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2040 times.
✓ Branch 8 taken 71 times.
✓ Branch 9 taken 2040 times.
2111 if (f.isNull() || !f.tag()) {
145
2/4
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✗ Branch 3 not taken.
71 UseCachedProbeDuration(*this, fullMediaPath);
146 71 return;
147 }
148
149
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 TagLib::Tag* tag = f.tag();
150
151
3/6
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
2040 title = tag->title().toCString(true);
152
3/6
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
2040 artist = tag->artist().toCString(true);
153
3/6
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
2040 album = tag->album().toCString(true);
154
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 year = tag->year();
155
3/6
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
2040 comment = tag->comment().toCString(true);
156
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 track = tag->track();
157
3/6
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2040 times.
✗ Branch 5 not taken.
2040 genre = tag->genre().toCString(true);
158
159
2/4
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
2040 if (f.audioProperties()) {
160
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 TagLib::AudioProperties* properties = f.audioProperties();
161
162
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 lengthMS = properties->lengthInMilliseconds();
163 2040 length = lengthMS / 1000;
164 2040 seconds = length % 60;
165 2040 minutes = (length - seconds) / 60;
166
167
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 bitrate = properties->bitrate();
168
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 sampleRate = properties->sampleRate();
169
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 channels = properties->channels();
170 }
171
2/4
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2040 times.
✗ Branch 3 not taken.
2040 UseCachedProbeDuration(*this, fullMediaPath);
172 2040 length = lengthMS / 1000;
173 2040 seconds = length % 60;
174 2040 minutes = (length - seconds) / 60;
175
176
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Title : %s\n", title.c_str());
177
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Artist : %s\n", artist.c_str());
178
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Album : %s\n", album.c_str());
179
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Year : %d\n", year);
180
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Comment : %s\n", comment.c_str());
181
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Track : %d\n", track);
182
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Genre : %s\n", genre.c_str());
183
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Properties:\n");
184
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Length : %d\n", length);
185
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Seconds : %d\n", seconds);
186
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Minutes : %d\n", minutes);
187
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Bitrate : %d\n", bitrate);
188
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Sample Rate: %d\n", sampleRate);
189
1/2
✓ Branch 0 taken 2040 times.
✗ Branch 1 not taken.
2040 LogDebug(VB_MEDIAOUT, " Channels : %d\n", channels);
190
191 2040 return;
192 2111 }
193