From e189db04389377582047b1250711b6f30d971218 Mon Sep 17 00:00:00 2001 From: dotMavriQ Date: Sun, 30 Nov 2025 22:29:08 +0000 Subject: [PATCH] feat: add batch import for bulk scrobbling - New batch module for importing scrobbles from files - Support CSV and JSON formats - Add --import flag to import from file - Add --create-import flag to generate example files - Rate limiting (1 second between scrobbles) - Automatic stats and history recording - Confirmation prompt before importing Example usage: demel --create-import # creates example files demel --import songs.csv # imports from CSV --- main.lua | 65 +++++++++++++++++++++++++ src/batch.lua | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/batch.lua diff --git a/main.lua b/main.lua index d284d4b..e625e47 100644 --- a/main.lua +++ b/main.lua @@ -17,6 +17,7 @@ local listenbrainz = require "listenbrainz" local cache = require "cache" local stats = require "stats" local history = require "history" +local batch = require "batch" -- === CLI ARGUMENTS === @@ -35,6 +36,8 @@ Options: --export [file] Export stats to CSV --history [n] Show recent search history (default: 10) --clear-history Clear search history + --import Import scrobbles from CSV or JSON file + --create-import Create example import file Environment Variables: DEMEL_LOG_LEVEL Set log verbosity (0=SILENT, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) @@ -81,6 +84,18 @@ for i, arg in ipairs(arg) do elseif arg == "--clear-history" then history.clear() os.exit(0) + elseif arg == "--import" then + local filename = arg[i + 1] + if not filename then + print("[ERROR] Please specify a file to import") + os.exit(1) + end + -- Handle import in special mode (needs modules loaded) + _G.BATCH_IMPORT_FILE = filename + elseif arg == "--create-import" then + batch.create_example_csv() + batch.create_example_json() + os.exit(0) end end @@ -96,6 +111,56 @@ print("----------------") if not gemini.check_connection() then os.exit(1) end if not listenbrainz.check_connection() then os.exit(1) end +-- Handle batch import if requested +if _G.BATCH_IMPORT_FILE then + print("\n\27[1mBatch Import Mode\27[0m") + print("----------------") + + local entries = batch.import_file(_G.BATCH_IMPORT_FILE) + if not entries or #entries == 0 then + print("[ERROR] No valid entries found in file") + os.exit(1) + end + + print(string.format("[INFO] Found %d entries to import", #entries)) + print("[INFO] This will scrobble them to ListenBrainz") + io.write("Continue? (y/N) > ") + local confirm = io.read() + + if confirm ~= "y" and confirm ~= "Y" then + print("Cancelled.") + os.exit(0) + end + + local success_count = 0 + local fail_count = 0 + + for i, entry in ipairs(entries) do + local artist = entry.artist + local title = entry.title + local album = entry.album or "Unknown Album" + local timestamp = entry.timestamp or os.time() + + io.write(string.format("[%d/%d] %s - %s... ", i, #entries, artist, title)) + + local ok = listenbrainz.submit_listen(artist, title, album, timestamp) + if ok then + stats.record_scrobble(artist, title, album, timestamp) + history.add_entry("batch import", artist, title, album) + print("✓") + success_count = success_count + 1 + -- Rate limit: wait 1 second between scrobbles + os.execute("sleep 1") + else + print("✗") + fail_count = fail_count + 1 + end + end + + print(string.format("\n[INFO] Import complete: %d success, %d failed", success_count, fail_count)) + os.exit(0) +end + print("\n\27[32mSystem Ready.\27[0m Type 'exit' or 'quit' to leave.") -- === HELPERS === diff --git a/src/batch.lua b/src/batch.lua new file mode 100644 index 0000000..4cad8e7 --- /dev/null +++ b/src/batch.lua @@ -0,0 +1,132 @@ +-- Batch import module for importing multiple scrobbles from file +local cjson = require "cjson" + +local M = {} + +function M.parse_csv(filename) + local file = io.open(filename, "r") + if not file then + print("[ERROR] Could not open file: " .. filename) + return nil + end + + local entries = {} + local line_num = 0 + + for line in file:lines() do + line_num = line_num + 1 + + -- Skip header line + if line_num == 1 then + goto continue + end + + -- Parse CSV line (simple parsing, doesn't handle quotes with commas) + local fields = {} + for field in line:gmatch("[^,]+") do + table.insert(fields, field:match("^%s*(.-)%s*$")) -- trim whitespace + end + + if #fields >= 2 then + table.insert(entries, { + artist = fields[1], + title = fields[2], + album = fields[3] or nil, + timestamp = tonumber(fields[4]) or os.time() + }) + end + + ::continue:: + end + + file:close() + return entries +end + +function M.parse_json(filename) + local file = io.open(filename, "r") + if not file then + print("[ERROR] Could not open file: " .. filename) + return nil + end + + local content = file:read("*a") + file:close() + + local status, data = pcall(cjson.decode, content) + if not status then + print("[ERROR] Invalid JSON format") + return nil + end + + -- Expected format: array of {artist, title, album?, timestamp?} + return data +end + +function M.import_file(filename, format) + -- Auto-detect format if not specified + if not format then + if filename:match("%.json$") then + format = "json" + elseif filename:match("%.csv$") then + format = "csv" + else + print("[ERROR] Unknown file format. Use .csv or .json") + return nil + end + end + + local entries + if format == "csv" then + entries = M.parse_csv(filename) + elseif format == "json" then + entries = M.parse_json(filename) + else + print("[ERROR] Unsupported format: " .. format) + return nil + end + + return entries +end + +function M.create_example_csv(filename) + filename = filename or "import_example.csv" + local file = io.open(filename, "w") + if not file then + print("[ERROR] Could not create file: " .. filename) + return false + end + + file:write("artist,title,album,timestamp\n") + file:write("Pink Floyd,Comfortably Numb,The Wall,\n") + file:write("Led Zeppelin,Stairway to Heaven,Led Zeppelin IV,\n") + file:write("Black Sabbath,Iron Man,Paranoid,\n") + + file:close() + print("[SUCCESS] Created example file: " .. filename) + print("[INFO] Edit this file and use: demel --import " .. filename) + return true +end + +function M.create_example_json(filename) + filename = filename or "import_example.json" + local file = io.open(filename, "w") + if not file then + print("[ERROR] Could not create file: " .. filename) + return false + end + + local example = { + {artist = "Pink Floyd", title = "Comfortably Numb", album = "The Wall"}, + {artist = "Led Zeppelin", title = "Stairway to Heaven", album = "Led Zeppelin IV"}, + {artist = "Black Sabbath", title = "Iron Man", album = "Paranoid"} + } + + file:write(cjson.encode(example)) + file:close() + print("[SUCCESS] Created example file: " .. filename) + print("[INFO] Edit this file and use: demel --import " .. filename) + return true +end + +return M