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 }