Added better debugging. Corrected Lidarr import. Updated README.md

This commit is contained in:
2025-09-28 06:57:46 +00:00
parent e40ec97435
commit 4658c38890
3 changed files with 595 additions and 347 deletions

222
README.md
View File

@@ -1,101 +1,185 @@
# MBID Poller # mbid-poller
This project provides a polling mechanism, packaged in a **Docker container**, to routinely check the MusicBrainz ID (MBID) status for items and then optionally trigger a search in a **Lidarr** instance. A polling script to automatically add artists to Lidarr based on MusicBrainz IDs (MBIDs). The script fetches artist data from the Lidarr.audio API and adds them to your Lidarr instance with proper monitoring and quality profiles.
--- With thanks to @kchiem for their perl script as the key piece of this image.
## 💡 Acknowledgements https://gist.github.com/kchiem/eb998ac3c6f5a96cbec03b8e8c3b21a6
The core logic for the polling mechanism in this project is based on an existing script generously shared by GitHub user **kchiem**. ## Features
We are utilizing and adapting the Perl script found in the following public Gist: - **Multiple Input Sources**: Support for API endpoints, JSON files, or single artist IDs
- **Robust Error Handling**: Automatic retries for API timeouts and comprehensive error logging
- **Validation**: Skips artists without albums to avoid cluttering your library
- **Configurable Logging**: Switch between info and debug logging levels
- **Docker Support**: Run as a containerized service
* **Original Script:** `kchiem/lidarr-ping.dist` ## Prerequisites
* **Link:** https://gist.github.com/kchiem/eb998ac3c6f5a96cbec03b8e8c3b21a6
We appreciate kchiem's contribution to the community! - Docker and Docker Compose
- A running Lidarr instance
- Lidarr API key (found in Lidarr Settings > General > API Key)
- Quality and Metadata profile IDs from your Lidarr instance
--- ## Configuration
## 🚀 Setup and Usage ### Environment Variables
This project is designed to run as a service alongside a **Lidarr** instance using Docker Compose. | Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `LIDARR_BASE` | URL to your Lidarr instance | Yes | `http://localhost:8686` |
| `LIDARR_API_KEY` | Your Lidarr API key | Yes | - |
| `LIDARR_ROOT_FOLDER` | Music root folder path in Lidarr | Yes | `/music` |
| `LIDARR_QUALITY_PROFILE_ID` | Quality profile ID from Lidarr | No | `1` |
| `LIDARR_METADATA_PROFILE_ID` | Metadata profile ID from Lidarr | No | `1` |
| `LOG_LEVEL` | Logging verbosity (`info` or `debug`) | No | `info` |
### 1. Prerequisites ### Input Sources (choose one)
Before starting, ensure you have: The script supports three input methods in order of priority:
1. **Docker** and **Docker Compose** installed. #### 1. API URL (Highest Priority)
2. A running **Lidarr** instance. ```yaml
3. The project files (`Dockerfile`, `poller.pl`, `docker-compose.yaml`, etc.) in a single directory. - MBID_API_URL=http://192.168.0.73:5110/artists
```
Fetches artist data from an API endpoint that returns JSON array with `foreignId` fields.
### 2. Configure Docker Compose #### 2. JSON File (Second Priority)
```yaml
- MBID_JSON_FILE=/config/ids.json
```
Reads from a local JSON file with the same format as the API.
The `mbid-poller` container needs to communicate with your Lidarr instance. #### 3. Single ID/URL (Lowest Priority)
```yaml
- MBID_URL=06f71b9e-639c-4903-bf6b-76b8893d3957
```
Processes a single MusicBrainz artist ID.
1. **Rename the Example:** Rename the provided `docker-compose.yaml.example` to `docker-compose.yaml`. ### JSON Format
2. **Network Setup:** Ensure the `lidarr` and `mbid-poller` services are on the **same Docker network** (e.g., `example_network`). If you use a different setup (like host network), update `LIDARR_BASE` accordingly.
3. **Lidarr Base URL:** The script uses the Lidarr API to automatically add the polled artists. You must set the following environment variables in the mbid-poller service:
| Environment Variable | Description | Source in Lidarr | For API or file input, use this JSON structure:
| :--- | :--- | :--- | ```json
| `LIDARR_BASE` | The internal URL of your Lidarr instance. | N/A (Docker network alias) | [
| `LIDARR_API_KEY` | Your Lidarr API key. | Settings > General > API Key | {
| `LIDARR_ROOT_FOLDER` | The path for your music root folder as seen by Lidarr. | Settings > Media Management > Root Folders | "foreignId": "06f71b9e-639c-4903-bf6b-76b8893d3957"
| `LIDARR_QUALITY_PROFILE_ID` | The numerical ID for the desired Quality Profile. (Default is 1) | Settings > Profiles > Quality Profiles (Inspect element to find ID) | },
| `LIDARR_METADATA_PROFILE_ID` | The numerical ID for the desired Metadata Profile. (Default is 1) | Settings > Profiles > Metadata Profiles (Inspect element to find ID) | {
"foreignId": "c85cfd6b-b1e9-4a50-bd55-eb725f04f7d5"
}
]
```
### 3. Choose the MBID Input Source ## Usage
The `poller.pl` script supports three different ways to provide MBIDs, in order of priority: ### Docker Compose
| Environment Variable | Priority | Description | 1. Create your `docker-compose.yaml`:
| :--- | :--- | :--- |
| `MBID_API_URL` | **1st** (Highest) | A URL that returns a JSON array of objects, each containing a `foreignId` field. |
| `MBID_JSON_FILE` | **2nd** | A path to a local JSON file (like `ids.json`) containing the MBID array. |
| `MBID_URL` | **3rd** (Lowest) | A single MusicBrainz URL or ID to process. |
In your `docker-compose.yaml`, **only one** of these should be uncommented and set. ```yaml
services:
mbid-poller:
build: .
container_name: mbid-poller
environment:
# Lidarr Configuration
- LIDARR_BASE=https://your-lidarr-instance.com
- LIDARR_API_KEY=your_api_key_here
- LIDARR_ROOT_FOLDER=/data/music
- LIDARR_QUALITY_PROFILE_ID=1
- LIDARR_METADATA_PROFILE_ID=1
# Logging
- LOG_LEVEL=info # Use 'debug' for troubleshooting
# Input Source (choose one)
- MBID_API_URL=http://your-api-server:5110/artists
# - MBID_JSON_FILE=/config/ids.json
# - MBID_URL=06f71b9e-639c-4903-bf6b-76b8893d3957
volumes:
- ./ids.json:/config/ids.json:ro # Only needed if using JSON file
restart: "no" # Run once
command: ["/app/poller.pl"]
```
#### A. Using a JSON File (Recommended for simple setups) 2. Build and run:
```bash
docker-compose up --build
```
1. Place your list of IDs in an `ids.json` file in the project directory (or adjust the volume mount). ### Finding Profile IDs
2. In `docker-compose.yaml`, **comment out** `MBID_API_URL` and **uncomment** `MBID_JSON_FILE`:
```yaml To find your Quality and Metadata Profile IDs:
# ... mbid-poller environment section
environment:
# ...
- LIDARR_BASE=http://lidarr:8686
- LIDARR_API_KEY=your_lidarr_api_key_here
- LIDARR_ROOT_FOLDER=/music # Example
- LIDARR_QUALITY_PROFILE_ID=1 # Example
- LIDARR_METADATA_PROFILE_ID=1 # Example
# - MBID_API_URL=
- MBID_JSON_FILE=/config/ids.json
# - MBID_URL=...
volumes:
- ./ids.json:/config/ids.json:ro # Maps your local file into the container
```
### 4. Build and Run 1. Go to Lidarr Settings > Profiles > Quality Profiles
2. Note the ID number in the URL when editing a profile
3. Go to Lidarr Settings > Profiles > Metadata Profiles
4. Note the ID number in the URL when editing a profile
With your files and `docker-compose.yaml` configured, you can build and run the poller. ## Logging
1. **Build the Container:** This compiles the Perl dependencies using the `Dockerfile`. ### Info Level (Default)
Clean output showing processing progress and results:
```
--- Processing (1/165): 00d0f0fa-a48c-416d-b4ff-25a290ce82d8 ---
- artist: Imagine Dragons
- artist already exists in Lidarr or validation failed
```
```bash ### Debug Level
docker compose build mbid-poller Verbose logging for troubleshooting:
``` ```
[DEBUG] attempt 1: making request...
[DEBUG] Lidarr API Response Code: 201
[INFO] SUCCESS: Artist added to Lidarr
```
2. **Run the Poller:** Since the `restart` policy is set to `"no"`, this command will run the script once and exit. Set `LOG_LEVEL=debug` to enable debug logging.
```bash ## How It Works
docker compose up mbid-poller
```
The script will fetch the IDs, then loop through them. For each ID, it will: 1. **Fetch Artist List**: Retrieves MBIDs from your configured source
2. **Query Lidarr.audio API**: Gets detailed artist information for each MBID
3. **Validate**: Checks if artist has albums (skips if none)
4. **Add to Lidarr**: Creates artist entry with monitoring enabled
5. **Handle Duplicates**: Skips artists that already exist
1. Ping the external MusicBrainz API until a successful response is received. ## Error Handling
2. Use the Lidarr API to automatically add the artist and set it to monitored.
3. Output a confirmation or error message before moving to the next artist. - **API Timeouts**: Automatic retry with exponential backoff (max 15 attempts)
- **Missing Data**: Validates required fields before processing
- **Duplicate Artists**: Gracefully handles artists already in Lidarr
- **Network Issues**: Comprehensive error logging with specific HTTP status codes
## Troubleshooting
### Common Issues
1. **Artists not being added**: Set `LOG_LEVEL=debug` to see detailed API responses
2. **Authentication errors**: Verify your `LIDARR_API_KEY` is correct
3. **Profile errors**: Ensure `LIDARR_QUALITY_PROFILE_ID` and `LIDARR_METADATA_PROFILE_ID` exist
4. **Path issues**: Check that `LIDARR_ROOT_FOLDER` exists and is writable
### Debug Mode
Enable debug logging to see:
- Full API request/response details
- Artist data field mapping
- Lidarr API payloads
- Step-by-step processing information
## Output
The script provides a summary at completion:
```
=== Processing Complete ===
Summary:
Total processed: 165
Successfully added: 142
Errors: 3
Skipped (validation failed): 20
```
## License
This project builds upon the original work by @kchiem and is provided as-is for personal use.

View File

@@ -3,40 +3,52 @@ services:
image: lscr.io/linuxserver/lidarr:latest image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr container_name: lidarr
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=${TZ} - TZ=${TZ}
volumes: volumes:
- /path/to/lidarr/config:/config - /path/to/lidarr/config:/config
- /path/to/data/:/data/ - /path/to/data/:/data/
- /path/to/downloads/:/downloads - /path/to/downloads/:/downloads
ports: ports:
- 8686:8686 - 8686:8686
networks: networks:
example_network: - music_network
ipv4_address: 172.18.0.1
restart: unless-stopped restart: unless-stopped
mbid-poller: mbid-poller:
image: gitea.kansaigaijin.com/KansaiGaijin image: gitea.kansaigaijin.com/kansaigaijin/mbid-poller:latest
container_name: mbid-poller container_name: mbid-poller
environment: environment:
- LIDARR_BASE=http://lidarr:8686 # The URL for your Lidarr instance # Lidarr Configuration
- LIDARR_API_KEY=your_lidarr_api_key_here # Get this from Lidarr Settings > General > API Key - LIDARR_BASE=http://lidarr:8686 # Internal Docker network URL
- LIDARR_ROOT_FOLDER=/music # Your music root folder path in Lidarr - LIDARR_API_KEY=your_lidarr_api_key_here # Get this from Lidarr Settings > General > API Key
- LIDARR_QUALITY_PROFILE_ID=1 # ID of quality profile (check Lidarr Settings > Profiles > Quality Profiles) - LIDARR_ROOT_FOLDER=/data/music # Your music root folder path in Lidarr
- LIDARR_METADATA_PROFILE_ID=1 - LIDARR_QUALITY_PROFILE_ID=1 # ID of quality profile (check Lidarr Settings > Profiles > Quality Profiles)
- MBID_API_URL= # OPTION 1: API URL (Current default, highest priority) - LIDARR_METADATA_PROFILE_ID=1 # ID of metadata profile
- MBID_JSON_FILE=/config/ids.json # OPTION 2: JSON File (To use, comment out MBID_API_URL and uncomment this lines)
- MBID_URL=https://musicbrainz.org/work/69755ab1-409e-3ad7-902f-3a839042799c # OPTION 3: Single ID/URL (To use, comment out the API URL and the JSON File lines) # Logging
- LOG_LEVEL=info # info or debug
# Input Source (choose ONE of the following options)
# OPTION 1: API URL (Highest Priority)
# - MBID_API_URL=http://your-api-server:port/api/path # see gitea.kansaigaijin.com/kansaigaijin/majola-lidarr-importer
# OPTION 2: JSON File (Second Priority) - CURRENTLY ACTIVE
- MBID_JSON_FILE=/config/ids.json
# OPTION 3: Single MBID (Lowest Priority)
# - MBID_URL=06f71b9e-639c-4903-bf6b-76b8893d3957 # Use MusicBrainz ID, not work URL
volumes: volumes:
- ./ids.json:/config/ids.json:ro - ./ids.json:/config/ids.json:ro # Mount your local ids.json file
networks: networks:
example_network: # Same network as Lidarr - music_network # Same network as Lidarr
ipv4_address: 172.18.0.2 restart: "no" # Run once
restart: "no" # Run once
command: ["/app/poller.pl"] command: ["/app/poller.pl"]
depends_on:
- lidarr # Wait for Lidarr to start first
networks: networks:
example_network: music_network:
external: true driver: bridge # Create network instead of using external

660
poller.pl
View File

@@ -1,255 +1,407 @@
#!/usr/bin/env perl #!/usr/bin/env perl
# -*- Perl -*- # -*- Perl -*-
use strict; use strict;
use warnings; use warnings;
use open qw(:std :encoding(UTF-8)); use open qw(:std :encoding(UTF-8));
use Data::Dumper qw(Dumper); use Data::Dumper qw(Dumper);
use Getopt::Std; use Getopt::Std;
use HTTP::Request; use HTTP::Request;
use IO::Handle; use IO::Handle;
use JSON::Tiny qw(decode_json encode_json); use JSON::Tiny qw(decode_json encode_json);
use LWP::UserAgent; use LWP::UserAgent;
use Time::Duration; use Time::Duration;
use URI::Escape; use URI::Escape;
# --- Environment Variables --- # --- Environment Variables ---
use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686'; use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686';
use constant LIDARR_API_KEY => $ENV{LIDARR_API_KEY} || die "FATAL: LIDARR_API_KEY environment variable is required\n"; use constant LIDARR_API_KEY => $ENV{LIDARR_API_KEY} || die "FATAL: LIDARR_API_KEY environment variable is required\n";
use constant LIDARR_ROOT_FOLDER => $ENV{LIDARR_ROOT_FOLDER} || '/music'; use constant LIDARR_ROOT_FOLDER => $ENV{LIDARR_ROOT_FOLDER} || '/music';
use constant LIDARR_QUALITY_PROFILE_ID => $ENV{LIDARR_QUALITY_PROFILE_ID} || 1; use constant LIDARR_QUALITY_PROFILE_ID => $ENV{LIDARR_QUALITY_PROFILE_ID} || 1;
use constant LIDARR_METADATA_PROFILE_ID => $ENV{LIDARR_METADATA_PROFILE_ID} || 1; use constant LIDARR_METADATA_PROFILE_ID => $ENV{LIDARR_METADATA_PROFILE_ID} || 1;
use constant LOG_LEVEL => lc($ENV{LOG_LEVEL} || 'info'); # info, debug
# ANSI escape sequences
sub ERASE_EOL { return "\033[K"; } # ANSI escape sequences
sub CURSOR_BACK { return "\033[${_[0]}D"; } sub ERASE_EOL { return "\033[K"; }
sub STATUS { return $_[0] . ERASE_EOL . CURSOR_BACK(length($_[0])) } sub CURSOR_BACK { return "\033[${_[0]}D"; }
sub STATUS { return $_[0] . ERASE_EOL . CURSOR_BACK(length($_[0])) }
getopts('bfhv');
# Logging functions
my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE sub debug_log {
my ($message) = @_;
# Define the list of IDs to process if (LOG_LEVEL eq 'debug') {
my @ids_to_process; print STDERR "[DEBUG] $message\n";
my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent }
my $json_content; }
# --- Input Logic: Prioritized Checks --- sub info_log {
print STDERR "=== MBID Poller Starting ===\n"; my ($message) = @_;
print STDERR "Environment variables:\n"; print STDERR "[INFO] $message\n";
print STDERR " LIDARR_BASE: " . (LIDARR_BASE) . "\n"; }
print STDERR " LIDARR_API_KEY: " . (LIDARR_API_KEY ? "***SET***" : "NOT SET") . "\n";
print STDERR " LIDARR_ROOT_FOLDER: " . (LIDARR_ROOT_FOLDER) . "\n"; sub error_log {
print STDERR " LIDARR_QUALITY_PROFILE_ID: " . (LIDARR_QUALITY_PROFILE_ID) . "\n"; my ($message) = @_;
print STDERR " LIDARR_METADATA_PROFILE_ID: " . (LIDARR_METADATA_PROFILE_ID) . "\n"; print STDERR "[ERROR] $message\n";
print STDERR " MBID_API_URL: " . ($ENV{MBID_API_URL} || 'NOT SET') . "\n"; }
print STDERR " MBID_JSON_FILE: " . ($ENV{MBID_JSON_FILE} || 'NOT SET') . "\n";
print STDERR " MBID_URL: " . ($ENV{MBID_URL} || 'NOT SET') . "\n"; getopts('bfhv');
print STDERR "\n";
my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE
# 1. Check for API URL (Highest Priority)
if (my $api_url = $ENV{MBID_API_URL}) { # Define the list of IDs to process
print STDERR "Fetching IDs from API URL: $api_url\n"; my @ids_to_process;
my $res = $ua->get($api_url); my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent
unless ($res->is_success) { my $json_content;
die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n";
} # --- Input Logic: Prioritized Checks ---
$json_content = $res->content; print STDERR "=== MBID Poller Starting ===\n";
print STDERR "Successfully fetched " . length($json_content) . " bytes from API\n"; print STDERR "Environment variables:\n";
} print STDERR " LIDARR_BASE: " . (LIDARR_BASE) . "\n";
# 2. Check for JSON File (Second Priority) print STDERR " LIDARR_API_KEY: " . (LIDARR_API_KEY ? "***SET***" : "NOT SET") . "\n";
elsif (my $json_file = $ENV{MBID_JSON_FILE}) { print STDERR " LIDARR_ROOT_FOLDER: " . (LIDARR_ROOT_FOLDER) . "\n";
print STDERR "Loading IDs from JSON file: $json_file\n"; print STDERR " LIDARR_QUALITY_PROFILE_ID: " . (LIDARR_QUALITY_PROFILE_ID) . "\n";
if (-f $json_file) { print STDERR " LIDARR_METADATA_PROFILE_ID: " . (LIDARR_METADATA_PROFILE_ID) . "\n";
open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!"; print STDERR " LOG_LEVEL: " . (LOG_LEVEL) . "\n";
$json_content = do { local $/; <$fh> }; print STDERR " MBID_API_URL: " . ($ENV{MBID_API_URL} || 'NOT SET') . "\n";
close $fh; print STDERR " MBID_JSON_FILE: " . ($ENV{MBID_JSON_FILE} || 'NOT SET') . "\n";
print STDERR "Successfully loaded " . length($json_content) . " bytes from file\n"; print STDERR " MBID_URL: " . ($ENV{MBID_URL} || 'NOT SET') . "\n";
} else { print STDERR "\n";
die "FATAL: JSON file $json_file does not exist\n";
} # 1. Check for API URL (Highest Priority)
} if (my $api_url = $ENV{MBID_API_URL}) {
# 3. Check for a single URL/ID (Lowest Priority) print STDERR "Fetching IDs from API URL: $api_url\n";
elsif (my $single_url = $ENV{MBID_URL}) { my $res = $ua->get($api_url);
print STDERR "Using single URL/ID: $single_url\n"; unless ($res->is_success) {
push @ids_to_process, $single_url; die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n";
} }
else { $json_content = $res->content;
die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n"; print STDERR "Successfully fetched " . length($json_content) . " bytes from API\n";
} }
# 2. Check for JSON File (Second Priority)
# --- JSON Parsing Logic (Applies to API URL and JSON File) --- elsif (my $json_file = $ENV{MBID_JSON_FILE}) {
if ($json_content) { print STDERR "Loading IDs from JSON file: $json_file\n";
print STDERR "Parsing JSON content...\n"; if (-f $json_file) {
my $data; open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!";
eval { $json_content = do { local $/; <$fh> };
$data = decode_json($json_content); close $fh;
}; print STDERR "Successfully loaded " . length($json_content) . " bytes from file\n";
if ($@) { } else {
die "Error decoding JSON from source: $@\n"; die "FATAL: JSON file $json_file does not exist\n";
} }
}
# Extract the 'foreignId' from each object in the array # 3. Check for a single URL/ID (Lowest Priority)
if (ref $data eq 'ARRAY') { elsif (my $single_url = $ENV{MBID_URL}) {
print STDERR "Found JSON array with " . scalar(@$data) . " items\n"; print STDERR "Using single URL/ID: $single_url\n";
foreach my $item (@$data) { push @ids_to_process, $single_url;
if (ref $item eq 'HASH' && exists $item->{foreignId}) { }
push @ids_to_process, $item->{foreignId}; else {
print STDERR " - Added ID: $item->{foreignId}\n"; die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n";
} else { }
warn "Skipping malformed item in input (missing 'foreignId').\n";
} # --- JSON Parsing Logic (Applies to API URL and JSON File) ---
} if ($json_content) {
} else { print STDERR "Parsing JSON content...\n";
die "Input content was not a JSON array.\n"; my $data;
} eval {
} $data = decode_json($json_content);
};
print STDERR "\nTotal IDs to process: " . scalar(@ids_to_process) . "\n"; if ($@) {
if (scalar(@ids_to_process) == 0) { die "Error decoding JSON from source: $@\n";
print STDERR "WARNING: No IDs found to process!\n"; }
exit 0;
} # Extract the 'foreignId' from each object in the array
if (ref $data eq 'ARRAY') {
# --- Main Processing Loop (CORRECTED TO CALL ADD FUNCTION) --- print STDERR "Found JSON array with " . scalar(@$data) . " items\n";
print STDERR "\n=== Starting Main Processing ===\n"; foreach my $item (@$data) {
foreach my $id (@ids_to_process) { if (ref $item eq 'HASH' && exists $item->{foreignId}) {
chomp $id; push @ids_to_process, $item->{foreignId};
print "\n--- Processing: $id ---\n"; print STDERR " - Added ID: $item->{foreignId}\n";
print STDERR "Processing ID: $id\n"; } else {
warn "Skipping malformed item in input (missing 'foreignId').\n";
my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID }
}
# 1. Ping API to get artist data } else {
my $artist_data = ping_api($type, $id); die "Input content was not a JSON array.\n";
}
# 2. Validate data (e.g., check for albums) }
my @warnings = validate($type, $artist_data);
if (@warnings) { print STDERR "\nTotal IDs to process: " . scalar(@ids_to_process) . "\n";
warn "Skipping artist $id (" . ($artist_data->{artistname} || 'Unknown Artist') . ") due to warnings: " . join(', ', @warnings) . "\n"; if (scalar(@ids_to_process) == 0) {
next; # Skip to the next ID if validation fails print STDERR "WARNING: No IDs found to process!\n";
} exit 0;
}
# 3. Add artist to Lidarr (This is the critical step you wanted)
add_artist_to_lidarr($artist_data); # Counters for summary
my $processed_count = 0;
# Print the link for reference my $success_count = 0;
print "- add-to-lidarr: " . my $error_count = 0;
LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n"; my $skip_count = 0;
}
# --- End of Main Processing Loop --- # --- Main Processing Loop (ENHANCED WITH BETTER ERROR HANDLING) ---
print STDERR "\n=== Starting Main Processing ===\n";
print STDERR "\n=== Processing Complete ===\n"; foreach my $id (@ids_to_process) {
chomp $id;
# --- Subroutines --- $processed_count++;
print "\n--- Processing ($processed_count/" . scalar(@ids_to_process) . "): $id ---\n";
sub ping_api { print STDERR "Processing ID: $id\n";
my ($type, $id) = @_;
my $ua = new LWP::UserAgent; my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID
$ua->timeout(30); # Set reasonable timeout per request
my $start = time(); # 1. Ping API to get artist data
my $loops = 0; my $artist_data = ping_api($type, $id);
my $api = "https://api.lidarr.audio/api/v0.4/$type/$id";
my $json; # Check if we actually got valid data
if (!$artist_data) {
print STDERR "Pinging $api\n"; error_log("Failed to get artist data for $id - skipping");
$error_count++;
while (1) { next;
$loops++; }
print STDERR STATUS("- attempt $loops");
my $res = $ua->get($api); # 2. Validate data (e.g., check for albums)
my $status = $res->code; my @warnings = validate($type, $artist_data);
if (@warnings) {
if ($res->is_success) { warn "Skipping artist $id (" . ($artist_data->{artistname} || 'Unknown Artist') . ") due to warnings: " . join(', ', @warnings) . "\n";
eval { $skip_count++;
$json = decode_json($res->content); next; # Skip to the next ID if validation fails
}; }
if ($@) {
chomp $@; # 3. Add artist to Lidarr (This is the critical step you wanted)
warn "Retrying: $@\n"; my $add_result = add_artist_to_lidarr($artist_data);
} elsif ($fresh_result and defined $res->header('cf-cache-status') and $res->header('cf-cache-status') eq 'STALE') { if ($add_result) {
$status .= ' STALE'; $success_count++;
print STDERR STATUS("- attempt $loops: $status (cache warming...)"); } else {
} else { $error_count++;
print STDERR ERASE_EOL; }
last; # Got fresh data, exit retry loop
} # Print the link for reference
} else { print "- add-to-lidarr: " .
print STDERR STATUS("- attempt $loops: $status - " . $res->status_line); LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n";
} }
# --- End of Main Processing Loop ---
sleep 5;
} print STDERR "\n=== Processing Complete ===\n";
print STDERR "Summary:\n";
my $elapsed = time() - $start; print STDERR " Total processed: $processed_count\n";
print "- ready ($loops attempts, " . duration($elapsed) . ")\n" print STDERR " Successfully added: $success_count\n";
if $loops > 1; print STDERR " Errors: $error_count\n";
if ($type eq 'artist') { print STDERR " Skipped (validation failed): $skip_count\n";
print "- artist: " . ($json->{artistname} || 'UNKNOWN') . "\n";
} else { # --- Subroutines ---
print "- album: " . ($json->{title} || 'UNKNOWN') . "\n";
} sub ping_api {
my ($type, $id) = @_;
return $json; my $ua = new LWP::UserAgent;
} $ua->timeout(30); # Set reasonable timeout per request
my $start = time();
sub validate { my $loops = 0;
my ($type, $json) = @_; my $max_attempts = 15; # Limit attempts to prevent infinite loops
my @warnings; my $api = "https://api.lidarr.audio/api/v0.4/$type/$id";
if ($type eq 'artist') { my $json;
# make sure there are albums
unless (exists $json->{Albums} and scalar @{$json->{Albums}}) { print STDERR "Pinging $api\n";
push(@warnings, 'no albums');
} while (1) {
} $loops++;
if (LOG_LEVEL eq 'debug') {
return @warnings; debug_log("attempt $loops: making request...");
} } else {
print STDERR STATUS("- attempt $loops");
sub add_artist_to_lidarr { }
my ($artist_data) = @_; my $res = $ua->get($api);
debug_log("attempt $loops: got response");
print STDERR "Adding artist to Lidarr: " . $artist_data->{artistname} . "\n"; my $status = $res->code;
my $ua = new LWP::UserAgent; if ($res->is_success) {
$ua->timeout(30); debug_log("attempt $loops: success, parsing JSON...");
eval {
# Prepare the payload for Lidarr API $json = decode_json($res->content);
my $payload = { };
'foreignArtistId' => $artist_data->{foreignArtistId}, if ($@) {
'artistName' => $artist_data->{artistname}, chomp $@;
'monitored' => JSON::Tiny::true, if (LOG_LEVEL eq 'debug') {
'monitorNewItems' => 'all', debug_log("attempt $loops: JSON parse error: $@");
'qualityProfileId' => LIDARR_QUALITY_PROFILE_ID + 0, # Ensure it's a number } else {
'metadataProfileId' => LIDARR_METADATA_PROFILE_ID + 0, # Ensure it's a number warn "Retrying: $@\n";
'path' => LIDARR_ROOT_FOLDER . '/' . $artist_data->{artistname}, }
'rootFolderPath' => LIDARR_ROOT_FOLDER, } elsif ($fresh_result and defined $res->header('cf-cache-status') and $res->header('cf-cache-status') eq 'STALE') {
'addOptions' => { $status .= ' STALE';
'monitor' => 'all', if (LOG_LEVEL eq 'debug') {
'searchForMissingAlbums' => JSON::Tiny::false debug_log("attempt $loops: $status (cache warming...)");
} } else {
}; print STDERR STATUS("- attempt $loops: $status (cache warming...)");
}
# Convert to JSON } else {
my $json_payload = JSON::Tiny::encode_json($payload); debug_log("attempt $loops: got valid data!");
if (LOG_LEVEL ne 'debug') {
# Make the API request print STDERR ERASE_EOL;
my $lidarr_api_url = LIDARR_BASE . '/api/v1/artist'; }
my $req = HTTP::Request->new(POST => $lidarr_api_url); last; # Got fresh data, exit retry loop
$req->header('Content-Type' => 'application/json'); }
$req->header('X-Api-Key' => LIDARR_API_KEY); } else {
$req->content($json_payload); if (LOG_LEVEL eq 'debug') {
debug_log("attempt $loops: $status - " . $res->status_line);
my $res = $ua->request($req); } else {
print STDERR STATUS("- attempt $loops: $status - " . $res->status_line);
if ($res->is_success) { }
print "- added to Lidarr successfully\n"; }
} elsif ($res->code == 400) {
# Artist might already exist # Break if we've exceeded max attempts
print "- artist already exists in Lidarr or validation failed\n"; if ($loops >= $max_attempts) {
} else { error_log("Failed after $max_attempts attempts");
die "Lidarr API request failed: " . $res->status_line . " - " . $res->content . "\n"; if (LOG_LEVEL ne 'debug') {
} print STDERR ERASE_EOL;
}
last;
}
debug_log("sleeping 5 seconds before retry...");
sleep 5;
}
# Check if we exhausted all attempts without success
if ($loops >= $max_attempts && !$json) {
error_log("Failed to get valid response after $max_attempts attempts");
return undef;
}
my $elapsed = time() - $start;
print "- ready ($loops attempts, " . duration($elapsed) . ")\n"
if $loops > 1;
if ($type eq 'artist') {
print "- artist: " . ($json->{artistname} || 'UNKNOWN') . "\n";
debug_log("Artist data keys: " . join(", ", keys %$json));
debug_log("foreignArtistId: " . ($json->{foreignArtistId} || 'NOT FOUND'));
debug_log("artistId: " . ($json->{artistId} || 'NOT FOUND'));
debug_log("id: " . ($json->{id} || 'NOT FOUND'));
} else {
print "- album: " . ($json->{title} || 'UNKNOWN') . "\n";
}
return $json;
}
sub validate {
my ($type, $json) = @_;
my @warnings;
if ($type eq 'artist') {
# make sure there are albums
unless (exists $json->{Albums} and scalar @{$json->{Albums}}) {
push(@warnings, 'no albums');
}
}
return @warnings;
}
sub add_artist_to_lidarr {
my ($artist_data) = @_;
info_log("Adding artist to Lidarr: " . ($artist_data->{artistname} || 'UNKNOWN'));
# Debug: show what fields we have
debug_log("Available fields in artist data: " . join(", ", keys %$artist_data));
# The foreignArtistId might be in a different field - let's check common possibilities
my $foreign_artist_id = $artist_data->{foreignArtistId} || $artist_data->{artistId} || $artist_data->{id};
# Enhanced validation of required fields
if (!$foreign_artist_id) {
error_log("Missing foreignArtistId/artistId/id in artist data");
debug_log("Artist data dump: " . Dumper($artist_data));
return 0;
}
if (!$artist_data->{artistname}) {
error_log("Missing artistname in artist data");
return 0;
}
my $ua = new LWP::UserAgent;
$ua->timeout(30);
# Clean artist name for path (remove invalid characters)
my $clean_artist_name = $artist_data->{artistname};
$clean_artist_name =~ s/[<>:"|?*\\\/]/_/g; # Replace invalid path characters
# Prepare the payload for Lidarr API
my $payload = {
'foreignArtistId' => $foreign_artist_id, # Use the found ID
'artistName' => $artist_data->{artistname},
'monitored' => JSON::Tiny::true,
'monitorNewItems' => 'all',
'qualityProfileId' => LIDARR_QUALITY_PROFILE_ID + 0, # Ensure it's a number
'metadataProfileId' => LIDARR_METADATA_PROFILE_ID + 0, # Ensure it's a number
'path' => LIDARR_ROOT_FOLDER . '/' . $clean_artist_name,
'rootFolderPath' => LIDARR_ROOT_FOLDER,
'addOptions' => {
'monitor' => 'all',
'searchForMissingAlbums' => JSON::Tiny::false
}
};
# Convert to JSON
my $json_payload;
eval {
$json_payload = JSON::Tiny::encode_json($payload);
};
if ($@) {
error_log("Failed to encode JSON payload: $@");
return 0;
}
debug_log("Lidarr API payload: " . substr($json_payload, 0, 200) . "...");
# Make the API request
my $lidarr_api_url = LIDARR_BASE . '/api/v1/artist';
debug_log("Making request to: $lidarr_api_url");
my $req = HTTP::Request->new(POST => $lidarr_api_url);
$req->header('Content-Type' => 'application/json');
$req->header('X-Api-Key' => LIDARR_API_KEY);
$req->content($json_payload);
my $res = $ua->request($req);
debug_log("Lidarr API Response Code: " . $res->code);
debug_log("Lidarr API Response Message: " . $res->message);
if ($res->is_success) {
print "- added to Lidarr successfully\n";
info_log("SUCCESS: Artist added to Lidarr");
# Try to parse response to get artist ID
if (my $response_data = eval { decode_json($res->content) }) {
debug_log("Added artist with Lidarr ID: " . ($response_data->{id} || 'unknown'));
}
return 1;
} elsif ($res->code == 400) {
# Artist might already exist or validation failed
print "- artist already exists in Lidarr or validation failed\n";
info_log("WARNING: " . $res->content);
# Try to parse error response
if (my $error_data = eval { decode_json($res->content) }) {
if (ref $error_data eq 'ARRAY' && @$error_data) {
info_log("Validation errors:");
foreach my $error (@$error_data) {
if (ref $error eq 'HASH') {
info_log(" - " . ($error->{errorMessage} || $error->{message} || "Unknown error"));
}
}
}
}
return 0;
} elsif ($res->code == 401) {
error_log("Authentication failed - check your API key");
return 0;
} elsif ($res->code == 404) {
error_log("Lidarr API endpoint not found - check your Lidarr URL and version");
return 0;
} else {
error_log("Lidarr API request failed: " . $res->status_line);
debug_log("Response body: " . $res->content);
return 0;
}
} }