1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 // * Redistributions of source code must retain the above copyright notice,
11 // this list of conditions and the following disclaimer.
12 //
13 // * Redistributions in binary form must reproduce the above copyright
14 // notice, this list of conditions and the following disclaimer in the
15 // documentation and/or other materials provided with the distribution.
16 //
17 // * Neither the name of the author nor the names of other contributors may
18 // be used to endorse or promote products derived from this software
19 // without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34
35 #include "Constants.h"
36 #include "GmeIndexerSource.h"
37 #include <musikcore/sdk/IDebug.h>
38 #include <musikcore/sdk/IPreferences.h>
39 #include <musikcore/sdk/String.h>
40 #include <musikcore/sdk/Filesystem.h>
41 #include <string>
42 #include <sstream>
43 #include <set>
44 #include <map>
45 #include <gme.h>
46
47 using namespace musik::core::sdk;
48
49 extern IDebug* debug;
50 extern IPreferences* prefs;
51
GmeIndexerSource()52 GmeIndexerSource::GmeIndexerSource() {
53 }
54
~GmeIndexerSource()55 GmeIndexerSource::~GmeIndexerSource() {
56 }
57
Release()58 void GmeIndexerSource::Release() {
59 delete this;
60 }
61
OnBeforeScan()62 void GmeIndexerSource::OnBeforeScan() {
63 this->filesIndexed = this->tracksIndexed = 0;
64 this->interrupt = false;
65 this->paths.clear();
66 }
67
OnAfterScan()68 void GmeIndexerSource::OnAfterScan() {
69 invalidFiles.clear();
70 }
71
Scan(IIndexerWriter * indexer,const char ** indexerPaths,unsigned indexerPathsCount)72 ScanResult GmeIndexerSource::Scan(
73 IIndexerWriter* indexer,
74 const char** indexerPaths,
75 unsigned indexerPathsCount)
76 {
77 /* keep these for later, for the removal phase */
78 for (size_t i = 0; i < indexerPathsCount; i++) {
79 this->paths.insert(fs::canonicalizePath(std::string(indexerPaths[i])));
80 }
81
82 auto checkFile = [this, indexer](const std::string& path) {
83 if (canHandle(path)) {
84 try {
85 this->UpdateMetadata(path, this, indexer);
86 }
87 catch (...) {
88 std::string error = str::format("error reading metadata for %s", path.c_str());
89 debug->Error(PLUGIN_NAME, error.c_str());
90 }
91 }
92 };
93
94 auto checkInterrupt = [this]() -> bool {
95 return this->interrupt;
96 };
97
98 for (auto& path : this->paths) {
99 if (!this->interrupt) {
100 fs::scanDirectory(std::string(path), checkFile, checkInterrupt);
101 }
102 }
103
104 indexer->CommitProgress(this, this->filesIndexed);
105
106 return ScanCommit;
107 }
108
Interrupt()109 void GmeIndexerSource::Interrupt() {
110 this->interrupt = true;
111 }
112
ScanTrack(IIndexerWriter * indexer,ITagStore * tagStore,const char * externalId)113 void GmeIndexerSource::ScanTrack(
114 IIndexerWriter* indexer,
115 ITagStore* tagStore,
116 const char* externalId)
117 {
118 std::string fn;
119 int trackNum;
120 if (indexer::parseExternalId(EXTERNAL_ID_PREFIX, std::string(externalId), fn, trackNum)) {
121 fn = fs::canonicalizePath(fn);
122
123 /* if the file doesn't exist anymore, or it was flagged as invalid,
124 we remove it */
125 if (!fs::fileExists(fn) || invalidFiles.find(fn) != invalidFiles.end()) {
126 indexer->RemoveByExternalId(this, externalId);
127 return;
128 }
129
130 /* otherwise, we remove it if it doesn't exist in the list of paths
131 we're supposed to be indexing */
132 for (auto& path : this->paths) {
133 if (fn.find(path) == 0) {
134 return; /* found a match, we're good */
135 }
136 }
137
138 indexer->RemoveByExternalId(this, externalId);
139 }
140 }
141
SourceId()142 int GmeIndexerSource::SourceId() {
143 return std::hash<std::string>()(PLUGIN_NAME);
144 }
145
UpdateMetadata(std::string fn,IIndexerSource * source,IIndexerWriter * indexer)146 void GmeIndexerSource::UpdateMetadata(
147 std::string fn,
148 IIndexerSource* source,
149 IIndexerWriter* indexer)
150 {
151 /* only need to do this check once, and it's relatively expensive because
152 it requires a db read. cache we've already done it. */
153 int modifiedTime = fs::getLastModifiedTime(fn);
154 const std::string firstExternalId = indexer::createExternalId(EXTERNAL_ID_PREFIX, fn, 0);
155 int modifiedDbTime = indexer->GetLastModifiedTime(this, firstExternalId.c_str());
156 if (modifiedDbTime < 0 || modifiedTime != modifiedDbTime) {
157 fn = fs::canonicalizePath(fn);
158
159 gme_t* data = nullptr;
160 gme_err_t err = gme_open_file(fn.c_str(), &data, gme_info_only);
161 if (err) {
162 debug->Error(PLUGIN_NAME, str::format("error opening %s", fn.c_str()).c_str());
163 invalidFiles.insert(fn);
164 }
165 else {
166 double minTrackLength = prefs->GetDouble(
167 KEY_MINIMUM_TRACK_LENGTH, DEFAULT_MINIMUM_TRACK_LENGTH);
168
169 bool ignoreSfx = prefs->GetBool(
170 KEY_EXCLUDE_SOUND_EFFECTS, DEFAULT_EXCLUDE_SOUND_EFFECTS);
171
172 if (prefs->GetBool(KEY_ENABLE_M3U, DEFAULT_ENABLE_M3U)) {
173 std::string m3u = getM3uFor(fn);
174 if (m3u.size()) {
175 err = gme_load_m3u(data, m3u.c_str());
176 if (err) {
177 debug->Error(PLUGIN_NAME, str::format("m3u found, but load failed '%s'", err).c_str());
178 }
179 }
180 }
181
182 const std::string defaultDuration =
183 std::to_string(prefs->GetDouble(
184 KEY_DEFAULT_TRACK_LENGTH,
185 DEFAULT_TRACK_LENGTH));
186
187 const std::string directory = fs::getDirectory<std::string>(fn);
188
189 for (int i = 0; i < gme_track_count(data); i++) {
190 const std::string externalId = indexer::createExternalId(EXTERNAL_ID_PREFIX, fn, i);
191 const std::string trackNum = std::to_string(i + 1);
192 const std::string defaultTitle = "Track " + std::to_string(1 + i);
193 const std::string modifiedTimeStr = std::to_string(modifiedTime);
194
195 auto track = indexer->CreateWriter();
196 track->SetValue("filename", externalId.c_str());
197 track->SetValue("directory", directory.c_str());
198 track->SetValue("filetime", modifiedTimeStr.c_str());
199 track->SetValue("track", trackNum.c_str());
200
201 gme_info_t* info = nullptr;
202 err = gme_track_info(data, &info, i);
203 if (err) {
204 debug->Error(PLUGIN_NAME, str::format("error getting track %d: %s", i, err).c_str());
205 track->SetValue("duration", defaultDuration.c_str());
206 track->SetValue("title", defaultTitle.c_str());
207 }
208 else if (info) {
209 /* don't index tracks that are shorter than the specified minimum length.
210 this allows users to ignore things like sound effects */
211 if (minTrackLength > 0.0 &&
212 ignoreSfx &&
213 info->length > 0 &&
214 info->length / 1000.0 < minTrackLength)
215 {
216 gme_free_info(info);
217 continue;
218 }
219
220 std::string duration = (info->length == -1)
221 ? defaultDuration
222 : std::to_string((float) info->play_length / 1000.0f);
223
224 track->SetValue("album", info->game);
225 track->SetValue("album_artist", info->system);
226 track->SetValue("genre", info->system);
227 track->SetValue("duration", duration.c_str());
228 track->SetValue("artist", strlen(info->author) ? info->author : info->system);
229 track->SetValue("title", strlen(info->song) ? info->song : defaultTitle.c_str());
230 }
231
232 gme_free_info(info);
233 indexer->Save(source, track, externalId.c_str());
234 track->Release();
235 ++tracksIndexed;
236 }
237 }
238
239 gme_delete(data);
240 }
241
242 /* we commit progress every so often */
243 if (++this->filesIndexed % 300 == 0) {
244 indexer->CommitProgress(this, this->filesIndexed + this->tracksIndexed);
245 this->filesIndexed = this->tracksIndexed = 0;
246 }
247 }