|
21 | 21 |
|
22 | 22 | #include <stdlib.h> |
23 | 23 | #include <string.h> |
| 24 | +#include <unordered_map> |
24 | 25 |
|
25 | 26 | #ifndef _WIN32 |
26 | 27 | #include <sys/stat.h> |
@@ -231,10 +232,151 @@ load_unsigned(const IniFile &file, const char *name, unsigned *value_r) |
231 | 232 | return true; |
232 | 233 | } |
233 | 234 |
|
| 235 | +static std::unordered_map<std::string, std::string> |
| 236 | +parse_ignore_list_line(std::string_view input) |
| 237 | +{ |
| 238 | + IgnoreListEntry ignore_list_entry; |
| 239 | + |
| 240 | + /* |
| 241 | + Format: tag1="value1" tag2="value2" ... |
| 242 | + Backslash escaping is supported. |
| 243 | + */ |
| 244 | + |
| 245 | + enum class ParserState { |
| 246 | + ExpectTagStart, |
| 247 | + InTag, |
| 248 | + ExpectQuote, |
| 249 | + InValue, |
| 250 | + InEscapeSequence |
| 251 | + } state = ParserState::ExpectTagStart; |
| 252 | + |
| 253 | + std::string current_tag; |
| 254 | + std::string current_value; |
| 255 | + std::unordered_map<std::string, std::string> result; |
| 256 | + |
| 257 | + for (size_t i = 0; i < input.length(); ++i) { |
| 258 | + char c = input[i]; |
| 259 | + |
| 260 | + switch (state) { |
| 261 | + case ParserState::ExpectTagStart: |
| 262 | + if (std::isspace(c)) continue; |
| 263 | + if (std::isalpha(c)) { |
| 264 | + current_tag = c; |
| 265 | + state = ParserState::InTag; |
| 266 | + } else { |
| 267 | + throw FormatRuntimeError("Error at position %d: expected tag start, got: '%c'", i, c); |
| 268 | + } |
| 269 | + break; |
| 270 | + |
| 271 | + case ParserState::InTag: |
| 272 | + if (std::isalpha(c)) { |
| 273 | + current_tag += c; |
| 274 | + } else if (c == '=') { |
| 275 | + state = ParserState::ExpectQuote; |
| 276 | + } else { |
| 277 | + throw FormatRuntimeError("Error at position %d: invalid tag character, got: '%c'", i, c); |
| 278 | + } |
| 279 | + break; |
| 280 | + |
| 281 | + case ParserState::ExpectQuote: |
| 282 | + if (c == '"') { |
| 283 | + current_value.clear(); |
| 284 | + state = ParserState::InValue; |
| 285 | + } else { |
| 286 | + throw FormatRuntimeError("Error at position %d: expected quote, got: '%c'", i, c); |
| 287 | + } |
| 288 | + break; |
| 289 | + |
| 290 | + case ParserState::InValue: |
| 291 | + if (c == '\\') { |
| 292 | + state = ParserState::InEscapeSequence; |
| 293 | + } else if (c == '"') { |
| 294 | + if (result.contains(current_tag)) { |
| 295 | + throw FormatRuntimeError("Error at position %d: tag '%s' is duplicated", i, current_tag.c_str()); |
| 296 | + } |
| 297 | + result.emplace(std::move(current_tag), std::move(current_value)); |
| 298 | + state = ParserState::ExpectTagStart; |
| 299 | + } else { |
| 300 | + current_value += c; |
| 301 | + } |
| 302 | + break; |
| 303 | + |
| 304 | + case ParserState::InEscapeSequence: |
| 305 | + current_value += c; |
| 306 | + state = ParserState::InValue; |
| 307 | + break; |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + if (state != ParserState::ExpectTagStart) { |
| 312 | + throw FormatRuntimeError("Unexpected end of line"); |
| 313 | + } |
| 314 | + |
| 315 | + return result; |
| 316 | +} |
| 317 | + |
| 318 | +static IgnoreList* |
| 319 | +load_ignore_list(const std::string& path, Config::IgnoreListMap& ignore_lists) |
| 320 | +{ |
| 321 | + |
| 322 | + FILE *file = fopen(path.c_str(), "r"); |
| 323 | + if (file == nullptr) { |
| 324 | + throw FormatRuntimeError("Cannot load ignore file: cannot open '%s' for reading", path.c_str()); |
| 325 | + } |
| 326 | + |
| 327 | + AtScopeExit(file) { fclose(file); }; |
| 328 | + |
| 329 | + IgnoreList ignore_list; |
| 330 | + |
| 331 | + { |
| 332 | + char line_buf[4096]; |
| 333 | + size_t line_num = 0; |
| 334 | + while (fgets(line_buf, sizeof(line_buf), file)) { |
| 335 | + std::string_view line(line_buf); |
| 336 | + if (line.back() == '\n') { |
| 337 | + line.remove_suffix(1); |
| 338 | + } |
| 339 | + |
| 340 | + line_num++; |
| 341 | + if (line.empty()) { |
| 342 | + continue; |
| 343 | + } |
| 344 | + |
| 345 | + try { |
| 346 | + auto parsed_line = parse_ignore_list_line(line); |
| 347 | + |
| 348 | + if (parsed_line.empty()) { |
| 349 | + continue; |
| 350 | + } |
| 351 | + |
| 352 | + IgnoreListEntry entry{}; |
| 353 | + |
| 354 | + for (auto& [tag, value] : parsed_line) { |
| 355 | +#define set_tag_entry(tagname) if (tag == #tagname) { entry.tagname = std::move(value); continue; } |
| 356 | + set_tag_entry(artist) |
| 357 | + set_tag_entry(album) |
| 358 | + set_tag_entry(title) |
| 359 | + set_tag_entry(track) |
| 360 | +#undef set_tag_entry |
| 361 | + throw FormatRuntimeError("Unsupported tag: '%s'", tag.c_str()); |
| 362 | + } |
| 363 | + |
| 364 | + ignore_list.entries.emplace_back(std::move(entry)); |
| 365 | + } catch (const std::runtime_error& error) { |
| 366 | + throw FormatRuntimeError("Error loading ignore list '%s': Error parsing line %d: %s", |
| 367 | + path.c_str(), line_num, error.what()); |
| 368 | + } |
| 369 | + } |
| 370 | + } |
| 371 | + |
| 372 | + return &(ignore_lists[path] = std::move(ignore_list)); |
| 373 | +} |
| 374 | + |
234 | 375 | static ScrobblerConfig |
235 | 376 | load_scrobbler_config(const Config &config, |
236 | 377 | const std::string §ion_name, |
237 | | - const IniSection §ion) |
| 378 | + const IniSection §ion, |
| 379 | + Config::IgnoreListMap& ignore_lists) |
238 | 380 | { |
239 | 381 | ScrobblerConfig scrobbler; |
240 | 382 |
|
@@ -270,6 +412,17 @@ load_scrobbler_config(const Config &config, |
270 | 412 | scrobbler.journal = get_default_cache_path(config); |
271 | 413 | } |
272 | 414 |
|
| 415 | + std::string ignore_list = GetStdString(section, "ignore"); |
| 416 | + if (!ignore_list.empty()) { |
| 417 | + if (auto existing_ignore_list = ignore_lists.find(ignore_list); existing_ignore_list != ignore_lists.end()) { |
| 418 | + scrobbler.ignore_list = &existing_ignore_list->second; |
| 419 | + } else { |
| 420 | + scrobbler.ignore_list = load_ignore_list(ignore_list, ignore_lists); |
| 421 | + } |
| 422 | + } else { |
| 423 | + scrobbler.ignore_list = nullptr; |
| 424 | + } |
| 425 | + |
273 | 426 | return scrobbler; |
274 | 427 | } |
275 | 428 |
|
@@ -300,7 +453,8 @@ load_config_file(Config &config, const char *path) |
300 | 453 |
|
301 | 454 | config.scrobblers.emplace_front(load_scrobbler_config(config, |
302 | 455 | section.first, |
303 | | - section.second)); |
| 456 | + section.second, |
| 457 | + config.ignore_lists)); |
304 | 458 | } |
305 | 459 | } |
306 | 460 |
|
|
0 commit comments