Compare commits
10 Commits
4e0696a0b4
...
main
Author | SHA1 | Date | |
---|---|---|---|
5ba3376b68 | |||
4658c38890 | |||
e40ec97435 | |||
dcb7c6b9b4 | |||
8b83cf7dcf | |||
00086249b9 | |||
2bcce54df7 | |||
61c4e1ee4e | |||
35afe1c492 | |||
f75b21ad1e |
215
README.md
215
README.md
@@ -1,90 +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://your-api-server:port/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:** Verify the `LIDARR_BASE` environment variable in the `mbid-poller` service points to your Lidarr instance.
|
|
||||||
|
|
||||||
```yaml
|
For API or file input, use this JSON structure:
|
||||||
# Example for the default setup:
|
```json
|
||||||
- LIDARR_BASE=http://lidarr:8686
|
[
|
||||||
```
|
{
|
||||||
|
"foreignId": "06f71b9e-639c-4903-bf6b-76b8893d3957"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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:
|
||||||
#### A. Using a JSON File (Recommended for simple setups)
|
mbid-poller:
|
||||||
|
build: .
|
||||||
1. Place your list of IDs in an `ids.json` file in the project directory (or adjust the volume mount).
|
container_name: mbid-poller
|
||||||
2. In `docker-compose.yaml`, **comment out** `MBID_API_URL` and **uncomment** `MBID_JSON_FILE`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ... mbid-poller environment section
|
|
||||||
environment:
|
environment:
|
||||||
# ...
|
# Lidarr Configuration
|
||||||
- LIDARR_BASE=http://lidarr:8686
|
- LIDARR_BASE=https://your-lidarr-instance.com
|
||||||
# - MBID_API_URL=
|
- LIDARR_API_KEY=your_api_key_here
|
||||||
- MBID_JSON_FILE=/config/ids.json
|
- LIDARR_ROOT_FOLDER=/data/music
|
||||||
# - MBID_URL=...
|
- 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:port/artists
|
||||||
|
# - MBID_JSON_FILE=/config/ids.json
|
||||||
|
# - MBID_URL=06f71b9e-639c-4903-bf6b-76b8893d3957
|
||||||
volumes:
|
volumes:
|
||||||
- ./ids.json:/config/ids.json:ro # Maps your local file into the container
|
- ./ids.json:/config/ids.json:ro # Only needed if using JSON file
|
||||||
```
|
restart: "no" # Run once
|
||||||
|
command: ["/app/poller.pl"]
|
||||||
|
```
|
||||||
|
|
||||||
### 4. Build and Run
|
2. Build and run:
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
With your files and `docker-compose.yaml` configured, you can build and run the poller.
|
### Finding Profile IDs
|
||||||
|
|
||||||
1. **Build the Container:** This compiles the Perl dependencies using the `Dockerfile`.
|
To find your Quality and Metadata Profile IDs:
|
||||||
|
|
||||||
```bash
|
1. Go to Lidarr Settings > Profiles > Quality Profiles
|
||||||
docker compose build mbid-poller
|
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
|
||||||
|
|
||||||
2. **Run the Poller:** Since the `restart` policy is set to `"no"`, this command will run the script once and exit.
|
## Logging
|
||||||
|
|
||||||
```bash
|
### Info Level (Default)
|
||||||
docker compose up mbid-poller
|
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
|
||||||
|
```
|
||||||
|
|
||||||
The script will fetch the IDs, then loop through them, pinging the external MusicBrainz API until a successful response is received. It then outputs a Lidarr search URL for you to manually or automatically trigger the import in Lidarr.
|
### Debug Level
|
||||||
|
Verbose logging for troubleshooting:
|
||||||
|
```
|
||||||
|
[DEBUG] attempt 1: making request...
|
||||||
|
[DEBUG] Lidarr API Response Code: 201
|
||||||
|
[INFO] SUCCESS: Artist added to Lidarr
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `LOG_LEVEL=debug` to enable debug logging.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **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.
|
@@ -3,37 +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:
|
||||||
# Common: The URL for your Lidarr instance
|
# Lidarr Configuration
|
||||||
- LIDARR_BASE=http://lidarr:8686
|
- LIDARR_BASE=http://lidarr:8686 # Internal Docker network URL
|
||||||
- MBID_API_URL= # OPTION 1: API URL (Current default, highest priority)
|
- LIDARR_API_KEY=your_lidarr_api_key_here # Get this from Lidarr Settings > General > API Key
|
||||||
- MBID_JSON_FILE=/config/ids.json # OPTION 2: JSON File (To use, comment out MBID_API_URL and uncomment this lines)
|
- LIDARR_ROOT_FOLDER=/data/music # Your music root folder path in Lidarr
|
||||||
- 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)
|
- LIDARR_QUALITY_PROFILE_ID=1 # ID of quality profile (check Lidarr Settings > Profiles > Quality Profiles)
|
||||||
|
- LIDARR_METADATA_PROFILE_ID=1 # ID of metadata profile
|
||||||
|
|
||||||
|
# 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
|
560
poller.pl
560
poller.pl
@@ -1,153 +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 IO::Handle;
|
use HTTP::Request;
|
||||||
use JSON::Tiny qw(decode_json);
|
use IO::Handle;
|
||||||
use LWP::UserAgent;
|
use JSON::Tiny qw(decode_json encode_json);
|
||||||
use Time::Duration;
|
use LWP::UserAgent;
|
||||||
use URI::Escape;
|
use Time::Duration;
|
||||||
|
use URI::Escape;
|
||||||
# --- Environment Variables ---
|
|
||||||
use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686';
|
# --- Environment Variables ---
|
||||||
|
use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686';
|
||||||
# ANSI escape sequences
|
use constant LIDARR_API_KEY => $ENV{LIDARR_API_KEY} || die "FATAL: LIDARR_API_KEY environment variable is required\n";
|
||||||
sub ERASE_EOL { return "\033[K"; }
|
use constant LIDARR_ROOT_FOLDER => $ENV{LIDARR_ROOT_FOLDER} || '/music';
|
||||||
sub CURSOR_BACK { return "\033[${_[0]}D"; }
|
use constant LIDARR_QUALITY_PROFILE_ID => $ENV{LIDARR_QUALITY_PROFILE_ID} || 1;
|
||||||
sub STATUS { return $_[0] . ERASE_EOL . CURSOR_BACK(length($_[0])) }
|
use constant LIDARR_METADATA_PROFILE_ID => $ENV{LIDARR_METADATA_PROFILE_ID} || 1;
|
||||||
|
use constant LOG_LEVEL => lc($ENV{LOG_LEVEL} || 'info'); # info, debug
|
||||||
getopts('bfhv');
|
|
||||||
|
# ANSI escape sequences
|
||||||
my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE
|
sub ERASE_EOL { return "\033[K"; }
|
||||||
|
sub CURSOR_BACK { return "\033[${_[0]}D"; }
|
||||||
# Define the list of IDs to process
|
sub STATUS { return $_[0] . ERASE_EOL . CURSOR_BACK(length($_[0])) }
|
||||||
my @ids_to_process;
|
|
||||||
my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent
|
# Logging functions
|
||||||
my $json_content;
|
sub debug_log {
|
||||||
|
my ($message) = @_;
|
||||||
# --- Input Logic: Prioritized Checks ---
|
if (LOG_LEVEL eq 'debug') {
|
||||||
|
print STDERR "[DEBUG] $message\n";
|
||||||
# 1. Check for API URL (Highest Priority)
|
}
|
||||||
if (my $api_url = $ENV{MBID_API_URL}) {
|
}
|
||||||
print STDERR "Fetching IDs from API URL: $api_url\n";
|
|
||||||
my $res = $ua->get($api_url);
|
sub info_log {
|
||||||
unless ($res->is_success) {
|
my ($message) = @_;
|
||||||
die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n";
|
print STDERR "[INFO] $message\n";
|
||||||
}
|
}
|
||||||
$json_content = $res->content;
|
|
||||||
}
|
sub error_log {
|
||||||
# 2. Check for JSON File (Second Priority)
|
my ($message) = @_;
|
||||||
elsif (my $json_file = $ENV{MBID_JSON_FILE}) {
|
print STDERR "[ERROR] $message\n";
|
||||||
print STDERR "Loading IDs from JSON file: $json_file\n";
|
}
|
||||||
open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!";
|
|
||||||
$json_content = do { local $/; <$fh> };
|
getopts('bfhv');
|
||||||
close $fh;
|
|
||||||
}
|
my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE
|
||||||
# 3. Check for a single URL/ID (Lowest Priority)
|
|
||||||
elsif (my $single_url = $ENV{MBID_URL}) {
|
# Define the list of IDs to process
|
||||||
push @ids_to_process, $single_url;
|
my @ids_to_process;
|
||||||
}
|
my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent
|
||||||
else {
|
my $json_content;
|
||||||
die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n";
|
|
||||||
}
|
# --- Input Logic: Prioritized Checks ---
|
||||||
|
print STDERR "=== MBID Poller Starting ===\n";
|
||||||
|
print STDERR "Environment variables:\n";
|
||||||
# --- JSON Parsing Logic (Applies to API URL and JSON File) ---
|
print STDERR " LIDARR_BASE: " . (LIDARR_BASE) . "\n";
|
||||||
if ($json_content) {
|
print STDERR " LIDARR_API_KEY: " . (LIDARR_API_KEY ? "***SET***" : "NOT SET") . "\n";
|
||||||
my $data;
|
print STDERR " LIDARR_ROOT_FOLDER: " . (LIDARR_ROOT_FOLDER) . "\n";
|
||||||
eval {
|
print STDERR " LIDARR_QUALITY_PROFILE_ID: " . (LIDARR_QUALITY_PROFILE_ID) . "\n";
|
||||||
$data = decode_json($json_content);
|
print STDERR " LIDARR_METADATA_PROFILE_ID: " . (LIDARR_METADATA_PROFILE_ID) . "\n";
|
||||||
};
|
print STDERR " LOG_LEVEL: " . (LOG_LEVEL) . "\n";
|
||||||
if ($@) {
|
print STDERR " MBID_API_URL: " . ($ENV{MBID_API_URL} || 'NOT SET') . "\n";
|
||||||
die "Error decoding JSON from source: $@\n";
|
print STDERR " MBID_JSON_FILE: " . ($ENV{MBID_JSON_FILE} || 'NOT SET') . "\n";
|
||||||
}
|
print STDERR " MBID_URL: " . ($ENV{MBID_URL} || 'NOT SET') . "\n";
|
||||||
|
print STDERR "\n";
|
||||||
# Extract the 'foreignId' from each object in the array
|
|
||||||
if (ref $data eq 'ARRAY') {
|
# 1. Check for API URL (Highest Priority)
|
||||||
foreach my $item (@$data) {
|
if (my $api_url = $ENV{MBID_API_URL}) {
|
||||||
if (ref $item eq 'HASH' && exists $item->{foreignId}) {
|
print STDERR "Fetching IDs from API URL: $api_url\n";
|
||||||
push @ids_to_process, $item->{foreignId};
|
my $res = $ua->get($api_url);
|
||||||
} else {
|
unless ($res->is_success) {
|
||||||
warn "Skipping malformed item in input (missing 'foreignId').\n";
|
die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n";
|
||||||
}
|
}
|
||||||
}
|
$json_content = $res->content;
|
||||||
} else {
|
print STDERR "Successfully fetched " . length($json_content) . " bytes from API\n";
|
||||||
die "Input content was not a JSON array.\n";
|
}
|
||||||
}
|
# 2. Check for JSON File (Second Priority)
|
||||||
}
|
elsif (my $json_file = $ENV{MBID_JSON_FILE}) {
|
||||||
|
print STDERR "Loading IDs from JSON file: $json_file\n";
|
||||||
|
if (-f $json_file) {
|
||||||
# --- Main Processing Loop ---
|
open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!";
|
||||||
foreach my $id (@ids_to_process) {
|
$json_content = do { local $/; <$fh> };
|
||||||
chomp $id;
|
close $fh;
|
||||||
print "\n--- Processing: $id ---\n";
|
print STDERR "Successfully loaded " . length($json_content) . " bytes from file\n";
|
||||||
|
} else {
|
||||||
my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID
|
die "FATAL: JSON file $json_file does not exist\n";
|
||||||
|
}
|
||||||
my $json = ping_api($type, $id);
|
}
|
||||||
|
# 3. Check for a single URL/ID (Lowest Priority)
|
||||||
print "- add-to-lidarr: " .
|
elsif (my $single_url = $ENV{MBID_URL}) {
|
||||||
LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n";
|
print STDERR "Using single URL/ID: $single_url\n";
|
||||||
}
|
push @ids_to_process, $single_url;
|
||||||
|
}
|
||||||
## subroutines (ping_api and validate remain the same as your original script)
|
else {
|
||||||
|
die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n";
|
||||||
sub ping_api {
|
}
|
||||||
my ($type, $id) = @_;
|
|
||||||
my $ua = new LWP::UserAgent;
|
# --- JSON Parsing Logic (Applies to API URL and JSON File) ---
|
||||||
my $start = time();
|
if ($json_content) {
|
||||||
my $loops = 0;
|
print STDERR "Parsing JSON content...\n";
|
||||||
my $api = "https://api.lidarr.audio/api/v0.4/$type/$id";
|
my $data;
|
||||||
my $json;
|
eval {
|
||||||
|
$data = decode_json($json_content);
|
||||||
print STDERR "Pinging $api\n";
|
};
|
||||||
|
if ($@) {
|
||||||
while (1) {
|
die "Error decoding JSON from source: $@\n";
|
||||||
$loops++;
|
}
|
||||||
print STDERR STATUS("- attempt $loops");
|
|
||||||
my $res = $ua->get($api);
|
# Extract the 'foreignId' from each object in the array
|
||||||
my $status = $res->code;
|
if (ref $data eq 'ARRAY') {
|
||||||
if ($res->is_success) {
|
print STDERR "Found JSON array with " . scalar(@$data) . " items\n";
|
||||||
eval {
|
foreach my $item (@$data) {
|
||||||
$json = decode_json($res->content);
|
if (ref $item eq 'HASH' && exists $item->{foreignId}) {
|
||||||
};
|
push @ids_to_process, $item->{foreignId};
|
||||||
if ($@) {
|
print STDERR " - Added ID: $item->{foreignId}\n";
|
||||||
chomp $@;
|
} else {
|
||||||
warn "Retrying: $@\n";
|
warn "Skipping malformed item in input (missing 'foreignId').\n";
|
||||||
} elsif ($fresh_result and $res->header('cf-cache-status') eq 'STALE') {
|
}
|
||||||
$status .= ' STALE';
|
}
|
||||||
} else {
|
} else {
|
||||||
print STDERR ERASE_EOL;
|
die "Input content was not a JSON array.\n";
|
||||||
last;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
print STDERR STATUS("- attempt $loops: $status");
|
print STDERR "\nTotal IDs to process: " . scalar(@ids_to_process) . "\n";
|
||||||
sleep 5;
|
if (scalar(@ids_to_process) == 0) {
|
||||||
}
|
print STDERR "WARNING: No IDs found to process!\n";
|
||||||
|
exit 0;
|
||||||
my $elapsed = time() - $start;
|
}
|
||||||
print "- ready ($loops attempts, " . duration($elapsed) . ")\n"
|
|
||||||
if $loops > 1;
|
# Counters for summary
|
||||||
if ($type eq 'artist') {
|
my $processed_count = 0;
|
||||||
print "- artist: " . $json->{artistname} . "\n";
|
my $success_count = 0;
|
||||||
} else {
|
my $error_count = 0;
|
||||||
print "- album: " . $json->{title} . "\n";
|
my $skip_count = 0;
|
||||||
}
|
|
||||||
|
# --- Main Processing Loop (ENHANCED WITH BETTER ERROR HANDLING) ---
|
||||||
return $json;
|
print STDERR "\n=== Starting Main Processing ===\n";
|
||||||
}
|
foreach my $id (@ids_to_process) {
|
||||||
|
chomp $id;
|
||||||
sub validate {
|
$processed_count++;
|
||||||
my ($type, $json) = @_;
|
print "\n--- Processing ($processed_count/" . scalar(@ids_to_process) . "): $id ---\n";
|
||||||
my @warnings;
|
print STDERR "Processing ID: $id\n";
|
||||||
if ($type eq 'artist') {
|
|
||||||
# make sure there are albums
|
my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID
|
||||||
unless (exists $json->{Albums} and scalar @{$json->{Albums}}) {
|
|
||||||
push(@warnings, 'no albums');
|
# 1. Ping API to get artist data
|
||||||
}
|
my $artist_data = ping_api($type, $id);
|
||||||
|
|
||||||
|
# Check if we actually got valid data
|
||||||
|
if (!$artist_data) {
|
||||||
|
error_log("Failed to get artist data for $id - skipping");
|
||||||
|
$error_count++;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Validate data (e.g., check for albums)
|
||||||
|
my @warnings = validate($type, $artist_data);
|
||||||
|
if (@warnings) {
|
||||||
|
warn "Skipping artist $id (" . ($artist_data->{artistname} || 'Unknown Artist') . ") due to warnings: " . join(', ', @warnings) . "\n";
|
||||||
|
$skip_count++;
|
||||||
|
next; # Skip to the next ID if validation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Add artist to Lidarr (This is the critical step you wanted)
|
||||||
|
my $add_result = add_artist_to_lidarr($artist_data);
|
||||||
|
if ($add_result) {
|
||||||
|
$success_count++;
|
||||||
|
} else {
|
||||||
|
$error_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print the link for reference
|
||||||
|
print "- add-to-lidarr: " .
|
||||||
|
LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n";
|
||||||
|
}
|
||||||
|
# --- End of Main Processing Loop ---
|
||||||
|
|
||||||
|
print STDERR "\n=== Processing Complete ===\n";
|
||||||
|
print STDERR "Summary:\n";
|
||||||
|
print STDERR " Total processed: $processed_count\n";
|
||||||
|
print STDERR " Successfully added: $success_count\n";
|
||||||
|
print STDERR " Errors: $error_count\n";
|
||||||
|
print STDERR " Skipped (validation failed): $skip_count\n";
|
||||||
|
|
||||||
|
# --- Subroutines ---
|
||||||
|
|
||||||
|
sub ping_api {
|
||||||
|
my ($type, $id) = @_;
|
||||||
|
my $ua = new LWP::UserAgent;
|
||||||
|
$ua->timeout(30); # Set reasonable timeout per request
|
||||||
|
my $start = time();
|
||||||
|
my $loops = 0;
|
||||||
|
my $max_attempts = 15; # Limit attempts to prevent infinite loops
|
||||||
|
my $api = "https://api.lidarr.audio/api/v0.4/$type/$id";
|
||||||
|
my $json;
|
||||||
|
|
||||||
|
print STDERR "Pinging $api\n";
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
$loops++;
|
||||||
|
if (LOG_LEVEL eq 'debug') {
|
||||||
|
debug_log("attempt $loops: making request...");
|
||||||
|
} else {
|
||||||
|
print STDERR STATUS("- attempt $loops");
|
||||||
|
}
|
||||||
|
my $res = $ua->get($api);
|
||||||
|
debug_log("attempt $loops: got response");
|
||||||
|
my $status = $res->code;
|
||||||
|
|
||||||
|
if ($res->is_success) {
|
||||||
|
debug_log("attempt $loops: success, parsing JSON...");
|
||||||
|
eval {
|
||||||
|
$json = decode_json($res->content);
|
||||||
|
};
|
||||||
|
if ($@) {
|
||||||
|
chomp $@;
|
||||||
|
if (LOG_LEVEL eq 'debug') {
|
||||||
|
debug_log("attempt $loops: JSON parse error: $@");
|
||||||
|
} else {
|
||||||
|
warn "Retrying: $@\n";
|
||||||
|
}
|
||||||
|
} elsif ($fresh_result and defined $res->header('cf-cache-status') and $res->header('cf-cache-status') eq 'STALE') {
|
||||||
|
$status .= ' STALE';
|
||||||
|
if (LOG_LEVEL eq 'debug') {
|
||||||
|
debug_log("attempt $loops: $status (cache warming...)");
|
||||||
|
} else {
|
||||||
|
print STDERR STATUS("- attempt $loops: $status (cache warming...)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug_log("attempt $loops: got valid data!");
|
||||||
|
if (LOG_LEVEL ne 'debug') {
|
||||||
|
print STDERR ERASE_EOL;
|
||||||
|
}
|
||||||
|
last; # Got fresh data, exit retry loop
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (LOG_LEVEL eq 'debug') {
|
||||||
|
debug_log("attempt $loops: $status - " . $res->status_line);
|
||||||
|
} else {
|
||||||
|
print STDERR STATUS("- attempt $loops: $status - " . $res->status_line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Break if we've exceeded max attempts
|
||||||
|
if ($loops >= $max_attempts) {
|
||||||
|
error_log("Failed after $max_attempts attempts");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user