Note: Any field wrapped in
<...>(e.g.<GOOGLE_API_KEY>,<username>) is a placeholder — replace it with your own value before use. All API keys referenced below are free to obtain.
lidarr: # To use Docker Mods you need to run this as root
image: lscr.io/linuxserver/lidarr:nightly # I use nightly here for the plugin functionality
container_name: lidarr
environment:
- PUID=1000 # This should be the user that owns the music library
- PGID=13000 # And the group used across my other container
- UMASK=002
- TZ=Europe/Berlin
- DOCKER_MODS=linuxserver/mods:universal-package-install
- INSTALL_PIP_PACKAGES=git+https://github.com/beetbox/beets.git|git+https://github.com/MxMarx/gazelle-origin@orpheus|git+https://github.com/MxMarx/beets-originquery|pyacoustid|beetcamp|pylast|python3-discogs-client|xtractor|/beets/beetsplug/commaartist|/beets/beetsplug/artistalias
# /beets/beetsplug/commaartist and /beets/beetsplug/artistalias are custom plugins made by me
# https://github.com/acealone/beets-commaartist
# https://github.com/acealone/beets-artistalias
- INSTALL_PACKAGES=git|ffmpeg|flac|imagemagick
volumes:
- ".config:/config"
- "/hdd/data:/data" # Replace /hdd/data with your musics folder
- "/home/<username>/.config/beets:/beets" # Path to your beets directory (this is optional)
ports:
- "8686:8686"
labels:
- "org.hotio.pullio.notify=true"
- "org.hotio.pullio.update=true"
- "org.hotio.pullio.discord.webhook=<your-discord-webhook-url>"
restart: unless-stoppedAdds a bunch of features to Lidarr like Soulseek integration, YouTube Downloader, Queue Cleaner and Search Sniper.
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
user: "13012:13000" # I run this with a dedicated user (rootless)
environment:
- PUID=13012
- PGID=13000 # Same group as above for permissions
- UMASK=002
- ND_PLUGINS_ENABLED=true
- ND_PLUGINS_AUTORELOAD=true
- ND_AGENTS=audiomuseai,lastfm,deezer
- ND_DEVARTISTINFOTIMETOLIVE=1s
- ND_LASTFM_APIKEY=<your-api-key>
- ND_LASTFM_SECRET=<your-api-secret>
ports:
- "4533:4533"
volumes:
- "./navidrome/config:/data"
- "/hdd/data/media/music:/music:ro"
labels:
- "org.hotio.pullio.notify=true"
- "org.hotio.pullio.update=true"
- "org.hotio.pullio.discord.webhook="
restart: unless-stoppedLooks for added files, analyzes the mood and writes them to the metadata.
https://github.com/WB2024/Essentia-to-Metadata/tree/main
[Unit]
Description=Essentia Music Tagger - File Watcher Service
Documentation=https://github.com/your-repo/essentia-to-metadata
After=network.target local-fs.target
[Service]
Type=simple
User=root
Group=root
# Working directory
WorkingDirectory=/opt/apps/essentia-to-metadata
# Environment variables - customize these for your setup
Environment="WATCH_DIR=/data/media/music"
Environment="TAGGER_SCRIPT=/opt/apps/essentia-to-metadata/tag_music.py"
Environment="VENV_PATH=/opt/apps/essentia-to-metadata/venv"
Environment="MODEL_DIR=/opt/apps/essentia-to-metadata/models"
Environment="LOG_DIR=/var/log/essentia-tagger"
Environment="DEBOUNCE_SECONDS=5"
Environment="COOLDOWN_SECONDS=120"
Environment="GENRES=3"
Environment="GENRE_THRESHOLD=15"
Environment="MOOD_THRESHOLD=0.5"
Environment="GENRE_FORMAT=parent_child"
Environment="DRY_RUN=false"
Environment="OVERWRITE=true"
# The watcher script
ExecStart=/opt/apps/essentia-to-metadata/essentia_watcher.sh
# Restart policy
Restart=always
RestartSec=10
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=essentia-tagger
[Install]
WantedBy=multi-user.target# Global Options ##################################################################################
directory: /data/media/music # Destination.
library: library.db # Database destination
pluginpath: /home/<username>/.config/beets # I have beets installed on the host. This may vary for you.
include: # A list of extra configuration files to include.
- plugins.yaml # Activate/deactivate plugins here.
# - secrets.yaml # Secret strings (REDACTED) like usernames and passwords. Add this file to git.ignore.
original_date: yes # Use the original date for the release.
per_disc_numbering: yes # The track numbers are incremented throughout a multi disk release.
threaded: yes # Indicating whether the autotagger should use multiple threads.
# This makes things substantially faster by overlapping work.
###################################################################################################
# Paths ###########################################################################################
asciify_path: yes # Convert all non-ASCII characters in paths to ASCII equivalents.
max_filename_length: 255 # 0 = unlimited.
ignore: ["*.ogg","*.m4v"]
paths: # Directory and naming scheme.
# The aunique{} function ensures that identically-named albums are placed in different directories.
default: $albumartist/$album%aunique{}/%if{$multidisc,$disc.}$track - $title
singleton: $albumartist/$album%aunique{}/%if{$multidisc,$disc.}$track - $title
comp: Compilations/$album%aunique{}/%if{$multidisc,$disc.}$track - $title
albumtype:soundtrack: Soundtracks/$original_year - $album%aunique{}/%if{$multidisc,$disc.}$track - $title
# copyartifacts ###############################
ext:jpg: $albumpath/scans/cover
ext:png: $albumpath/scans/cover
ext:pdf: $albumpath/scans/booklet
###############################################
##################################################################################################
# Import ##########################################################################################
import: # Beets can move or copy files but it doesn't make sense to do both.
write: yes # Controlling whether metadata (e.g., ID3) tags are written to files when using beet import.
copy: no # Keep your current directory structure.
# The option is ignored if move is enabled (i.e., beets can move or copy files but it doesn't make sense to do both).
move: no # Move the files. Otherwise there will be duplicates.
resume: no # Controls whether interrupted imports should be resumed.
# "yes" means that imports are always resumed when possible;
# "no" means resuming is disabled entirely;
# "ask" (the default) means that the user should be prompted when resuming is possible.
# incremental: no # Don't record imported directories.
# incremental_skip_later: no # Controlling whether imported directories are recorded and whether these recorded directories are skipped.
# from_scratch: no # Controlling whether existing metadata is discarded when a match is applied.
quiet_fallback: asis # Either skip (default) or asis, specifying what should happen in quiet mode when there is no strong recommendation.
# none_rec_action: ask # Either ask (default), asis or skip.
# Specifies what should happen during an interactive import session when there is no recommendation.
# Useful when you are only interested in processing medium and strong recommendations interactively.
# timid: no # Controlling whether the importer runs in timid mode.
# quiet: no # Controlling whether the importer runs in timid mode,
# in which it asks for confirmation on every autotagging match, even the ones that seem very close.
# log: /mnt/internal/music/import.log
# default_action: apply # One of apply, skip, asis, or none, indicating which option should be the default when selecting an action for a given match.
# This is the action that will be taken when you type return without an option letter.
languages: en # Prefer transliterated English names.
detail: no # Whether the importer UI should show detailed information about each match it finds.
# When enabled, this mode prints out the title of every track, regardless of whether it matches the original metadata.
# The default behavior only shows changes. Default: no.
# group_albums: no # By default, the beets importer groups tracks into albums based on the directories they reside in.
# This option instead uses files' metadata to partition albums.
# Enable this option if you have directories that contain tracks from many albums mixed together.
autotag: yes # If most of your collection consists of obscure music,
# you may be interested in disabling autotagging by setting this option to no.
duplicate_action: remove # Either skip, keep, remove, merge or ask. Controls how duplicates are treated in import task.
# "skip" means that new item (album or track) will be skipped;
# "keep" means keep both old and new items;
# "remove" means remove old item;
# "merge" means merge into one album;
# "ask" means the user should be prompted for the action each time.
# bell: yes # Ring the terminal bell to get your attention when the importer needs your input.
importadded:
preserve_mtimes: no # After importing files, re-set their mtimes to their original value. Default: no.
preserve_write_mtimes: no # After writing files, re-set their mtimes to their original value. Default: no.
badfiles:
check_on_import: no
extrafiles:
patterns:
all: '*.*'
# Metadata sources ################################################################################
## musicbrainz ##################################
musicbrainz:
# user: REDACTED
# pass: REDACTED
searchlimit: 20 # Recommendation from: https://github.com/kernitus/beets-oldestdate
extra_tags: # Enable improved MusicBrainz queries from tags.
[
catalognum,
country,
label,
media,
year
]
#################################################
originquery: # Get tags from gazelle-origin
origin_file: origin-*.yaml
use_origin_on_conflict: yes
tag_patterns:
media: '$.Media'
year: '$."Edition year"'
label: '$."Record label"'
catalognum: '$."Catalog number"'
albumdisambig: '$.Edition'
## chroma #######################################
chroma: # Turning on fingerprinting can increase the accuracy of the autotagger - especially on files with very poor metadata.
auto: yes # The Acoustid plugin extends the autotagger to use acoustic fingerprinting to find information for arbitrary audio.
# Install that plugin if you're willing to spend a little more CPU power to get tags for unidentified albums.
# (But be aware that it does slow down the process.)
## parentwork ###################################
parentwork: # This plugin adds seven tags:
# - parentwork: The title of the parent work.
# - mb_parentworkid: The MusicBrainz id of the parent work.
# - parentwork_disambig: The disambiguation of the parent work title.
# - parent_composer: The composer of the parent work.
# - parent_composer_sort: The sort name of the parent work composer.
# - work_date: The composition date of the work, or the first parent work that has a composition date. Format: yyyy-mm-dd.
force: no # As a default, parentwork only fetches work info for recordings that do not already have a parentwork tag.
# If force is enabled, it fetches it for all recordings. Default: no.
auto: yes # If enabled, automatically fetches works at import.
# It takes quite some time, because beets is restricted to one MusicBrainz query per second. Default: no.
## beetcamp #####################################
bandcamp: # Beetcamp. Uses the Bandcamp URL as id (for both albums and songs).
# If no matching release is found when importing you can select enter Id and paste the Bandcamp URL.
preferred_media: Digital # A comma-separated list of media to prioritise when fetching albums.
include_digital_only_tracks: true
# For media that isn't Digital Media, include all tracks,
# even if their titles contain digital only (or alike).
search_max: 10 # Maximum number of items to fetch through search queries. Default: 10.
art: true # Add a source to the FetchArt plugin to download album art for Bandcamp albums
# (requires FetchArt plugin enabled).
#exclude_extra_fields: # The data that is added after the core auto tagging process is considered extra:
#- lyrics # (currently) lyrics and comments (release description) fields.
#- comments # Since there yet isn't an easy way to preview them before they get applied,
# you can ignore them if you find them irrelevant or inaccurate.
#################################################
###################################################################################################
match:
strong_rec_thresh: 0.25 # Reflects the distance threshold below which beets will make a "strong recommendation" that the metadata be used.
# Strong recommendations are accepted automatically (except in "timid" mode),
# so you can use this to make beets ask your opinion more or less often.
# The threshold is a distance value between 0.0 and 1.0, so you can think of it as the opposite of a similarity value.
# For example, if you want to automatically accept any matches above 90% similarity, use: "strong_rec_thresh: 0.10"
# The default strong recommendation threshold is 0.04.
# When a match is below the medium recommendation threshold
# or the distance between it and the next-best match is above the gap threshold,
# the importer will suggest that match but not automatically confirm it.
# Otherwise, you'll see a list of options to choose from.
medium_rec_thresh: 0.125 # The medium_rec_thresh and rec_gap_thresh options work similarly.
distance_weights:
album_id: 0.01 # If tagging is enabled in Lidarr, the MusicBrainz ID might already exist. Lower the weight so beets has a chance to change it.
track_id: 0.01
# Lyrics ##########################################################################################
lyrics:
auto: yes # Fetch lyrics automatically during import. Default: yes.
fallback: '' # By default, the file will be left unchanged when no lyrics are found.
# Use the empty string '' to reset the lyrics in such a case.
# Default: None.
force: no # By default, beets won't fetch lyrics if the files already have ones.
# To instead always fetch lyrics, set the force option to yes.
# Default: no.
google_API_key: <GOOGLE_API_KEY>
# Your Google API key (to enable the Google Custom Search backend).
# Default: None.
#google_engine_ID: # The custom search engine to use.
# Default: The beets custom search engine, which gathers an updated list of sources known to be scrapeable.
genius_api_key: <GENIUS_API_KEY>
sources: # List of sources to search for lyrics.
# An asterisk * expands to all available sources.
# Both it and the genius source will only be enabled if BeautifulSoup is installed.
# - bandcamp # ToDo: Not sure if this entry is really necessary.
- genius
# - google # The google source will be automatically deactivated if no google_API_key is setup.
# - musixmatch # Possibly just 30% of a whole song text
# Leave in last position or comment it out.
# @test
###################################################################################################
# Pictures ########################################################################################
# In Roon, all the images embedded in the file tags are stored next to the audio files, or
# stored in a folder called artwork or scans next to the files, and are displayed.
# This includes all images that include cover, front or folder.
art_filename: cover # When importing album art, the name of the file (without extension) where the cover art image should be placed.
# This is a template string, so you can use any of the syntax available to Path Formats.
copyartifacts:
extensions: .jpg .pdf .png
print_ignored: yes
fetchart:
auto: yes # Enable automatic album art fetching during import.
cautious: yes # Pick only trusted album art by ignoring filenames that do not contain one of the keywords in "cover_names".
enforce_ratio: yes # Only allow images with 1:1 aspect ratio
minwidth: 300 # Only images with a width bigger or equal to minwidth are considered as valid album art candidates.
maxwidth: 3000 # A maximum image width to downscale fetched images if they are too big.
# The height is recomputed so that the aspect ratio is preserved.
sources: # An asterisk * expands to all available sources.
- filesystem # No remote art sources are queried if local art is found in the filesystem.
- lastfm
- itunes
- coverart
- albumart
- fanarttv
# - bandcamp
lastfm_key: <LAST_FM_API_KEY>
fanarttv_key: <FANARTTV_API_KEY> # API key to use for the fanart API.
store_source: yes # Store the art source (e.g. filesystem) in the beets database as art_source.
embedart:
auto: no # Enable automatic album art embedding.
compare_threshold: 50 # A threshold of 0 (the default) disables similarity checking and always embeds new images.
# Recommended between 10 and 100.
# The smaller the threshold number, the more similar the images must be.
ifempty: yes # Avoid embedding album art for files that already have art embedded.
maxwidth: 0 # A maximum width to downscale images before embedding them (the original image file is not altered).
# The resize operation reduces image width to at most maxwidth pixels.
# The height is recomputed so that the aspect ratio is preserved. See also Image Resizing for further caveats about image resizing.
remove_art_file: no # Automatically remove the album art file for the album after it has been embedded.
# This option is best used alongside the FetchArt plugin to download art with the purpose
# of directly embedding it into the file's metadata without an "intermediate" album art file.
###################################################################################################
# Last.fm #########################################################################################
# lastimport:
# per_page: 500 # The number of tracks to request from the API at once. Default: 500.
# retry_limit: 3 # How many times should we re-send requests to Last.fm on failure? Default: 3.
lastfm:
user: <LAST_FM_USERNAME>
api_key: <LAST_FM_API_KEY>
# types:
# play_count: int
# rating: float
lastgenre: # Fetches tags from Last.fm and assigns them as genres to your albums and items.
auto: yes # Fetch genres automatically during import. Default: yes.
# canonical: ~/.config/beets/genres/genres-tree.yaml
# Use a canonicalization tree. Setting this to yes will use a built-in tree.
# whitelist: ~/.config/beets/genres/genres.txt
# The filename of a custom genre list, yes to use the internal whitelist, or no to consider all genres valid.
# Default: yes.
count: 5 # Number of genres to fetch. Default: 1
# fallback: 'Pop/Rock' # A string to use as a fallback genre when no genre is found.
# You can use the empty string '' to reset the genre.
# Default: None.
separator: '; '
force: no # By default, beets will always fetch new genres, even if the files already have one.
# To instead leave genres in place when they pass the #whitelist: ~/.config/beets/genres.txt,
# set the force option to no.
min_weight: 10 # Minimum popularity factor below which genres are discarded. Default: 10.
prefer_specific: no # Sort genres by the most to least specific, rather than most to least popular. Default: no.
source: album # Which entity to look up in Last.fm. Can be either artist, album or track. Default: album.
title_case: yes # Convert the new tags to TitleCase before saving. Default: yes.
###################################################################################################
# ReplayGain ######################################################################################
replaygain:
auto: yes # Enable ReplayGain analysis during import. Default: yes.
# ReplayGain analysis is not fast, so you may want to disable it during import.
backend: ffmpeg # The analysis backend; either gstreamer, command, or audiotools. Default: command.
overwrite: no # Re-analyze files that already have ReplayGain tags. Default: no.
targetlevel: 89 # A number of decibels for the target loudness level. Default: 89.
per_disc: no # Calculate album ReplayGain on disc level instead of album level. Default: no.
###################################################################################################
# Maintenance #####################################################################################
duplicates:
album: no # List duplicate albums instead of tracks. Default: no.
checksum: ffmpeg -i {file} -f crc -
# Use an arbitrary command to compute a checksum of items.
# This overrides the keys option the first time it is run;
# however, because it caches the resulting checksum as flexattrs in the database,
# you can use --key=name_of_the_checksumming_program --key=any_other_keys
# (or set the keys configuration option) the second time around.
# Default: ffmpeg -i {file} -f crc -.
copy: none # A destination base directory into which to copy matched items.
# Default: none (disabled).
count: yes # Print a count of duplicate tracks or albums in the format
# $albumartist - $album - $title: $count (for tracks)
# or
# $albumartist - $album: $count (for albums).
# Default: no.
delete: no # Removes matched items from the library and from the disk. Default: no
format: format_item # A specific format with which to print every track or album.
# This uses the same template syntax as beets' path formats.
# The usage is inspired by, and therefore similar to, the list command.
# Default: format_item
full: yes # List every track or album that has duplicates, not just the duplicates themselves. Default: no
keys: [mb_trackid, mb_albumid]
# Define in which track or album fields duplicates are to be searched.
# By default, the plugin uses the MusicBrainz track and album IDs for this purpose.
# Using the keys option (as a YAML list in the configuration file,
# or as space-delimited strings in the command-line),
# you can extend this behavior to consider other attributes.
# Default: [mb_trackid, mb_albumid]
merge: no # Merge duplicate items by consolidating tracks and/or metadata where possible.
move: none # A destination base directory into which it will move matched items. Default: none (disabled).
path: yes # Output the path instead of metadata when listing duplicates. Default: no.
strict: no # Do not report duplicate matches if some of the attributes are not defined (i.e. null or empty). Default: no
#tag: no # A key=value pair.
# The plugin will add a new key attribute with value value as a flexattr to the database for duplicate items. Default: no.
tiebreak: {} # Dictionary of lists of attributes keyed by items or albums to use when choosing duplicates.
# By default, the tie-breaking procedure favors the most complete metadata attribute set.
# If you would like to consider the lower bitrates as duplicates, for example, set tiebreak: items: [bitrate].
# Default: {}.
missing:
#format: $albumartist - $album - $title
# A specific format with which to print every track.
# This uses the same template syntax as beets' path formats.
# The usage is inspired by, and therefore similar to, the list command.
# Default: format_item.
count: yes # Print a count of missing tracks per album, with format defaulting to $albumartist - $album: $missing.
# Default: no.
total: yes # Print a single count of missing tracks in all albums.
# Default: no.
###################################################################################################
# Permissions #####################################################################################
#permissions:
# file: 644
# dir: 755
###################################################################################################
# Inline #########################################################################################
#item_fields:
# clean_artist: |
# # 'artists' is a list field provided by beets/MusicBrainz
# # for tracks with multiple distinct artist entities.
# if artists:
# return ", ".join(artists)
#
# return artist
###################################################################################################
# Comma Artist ###################################################################################
commaartist:
auto: yes # Run automatically on import?
write: yes # Write changes to audio files?
albumartist: no # Process album artists by default?
###################################################################################################
# Artist Alias ###################################################################################
artistalias:
auto: yes
write: yes
albumartist: no
aliases:
"Ye": "Ye, Kanye West"
###################################################################################################
# UI ##############################################################################################
verbose: no
ui:
editor: nano
color: yes
colors:
text_success: green
text_warning: blue
text_error: red
text_highlight: blue
text_highlight_minor: lightgray
action_default: darkblue
action: purple
###################################################################################################
# HOOKS ###########################################################################################
hook:
hooks:
- event: album_imported
command: echo "\"{album}\""
- event: import
command: echo "imported from {paths}"
- event: art_set
command: echo "Coverart saved"
- event: import_begin
command: echo "Import started..."
- event: import_task_apply
command: echo "Metadata applied"
- event: item_copied
command: echo "\"{item}\" copied from \"{source}\" to \"{destination}\""
- event: item_moved
command: echo "Moved \"{item}\""
- event: write
command: echo "Writing to {path}"
- event: cli_exit
command: echo "All tasks finished!"
###################################################################################################Custom Lidarr import script that fetches origin metadata from Gazelle-based trackers (RED/OPS) and hands the album off to beet import.
import json
import logging
from logging.handlers import RotatingFileHandler
import os
import re
import subprocess
import requests
def parse_response(response):
try:
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logging.error(e)
return None
# %% Log messages to a file
# logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.basicConfig(handlers=[RotatingFileHandler('/config/logs/auto-beets.txt', maxBytes=1000000, backupCount=5)],
encoding='utf-8',
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d,%H:%M:%S',
level=logging.INFO
)
# %% Lidarr passes parameters to custom scripts as as environmental variables
# See https://wiki.servarr.com/lidarr/custom-scripts
lidarr = {
'eventtype': os.environ.get('lidarr_eventtype'),
'artist_name': os.environ.get('lidarr_artist_name'),
'artist_id': os.environ.get('lidarr_artist_id'),
'artist_mbid': os.environ.get('lidarr_artist_mbid'),
'album_title': os.environ.get('lidarr_album_title'),
'album_id': os.environ.get('lidarr_album_id'),
'album_mbid': os.environ.get('lidarr_album_mbid'),
'albumrelease_mbid': os.environ.get('lidarr_albumrelease_mbid'),
'torrent_hash': os.environ.get('lidarr_download_id'), # This is the torrent hash
'addedtrackpaths': os.environ.get('lidarr_addedtrackpaths'),
}
# Print a pretty table with lidarr parameters
msg = ["╔", "║", "║", "║", "╚"]
for key, val in lidarr.items():
if key in ['artist_name', 'album_title'] or logging.getLogger().isEnabledFor(logging.DEBUG):
w = max(len(key), len(str(val)))
msg[0] += "═" * (w + 2) + "╤"
msg[1] += f" {key.ljust(w)} │"
msg[2] += "─" * (w + 2) + "┼"
msg[3] += f" {str(val).ljust(w)} │"
msg[4] += "═" * (w + 2) + "╧"
msg[0] = msg[0][:-1] + "╗"
msg[1] = msg[1][:-1] + "║"
msg[2] = msg[2][:-1] + "║"
msg[3] = msg[3][:-1] + "║"
msg[4] = msg[4][:-1] + "╝"
logging.info("\n\nStarting auto-beets. Good luck!\n{0}\n".format('\n'.join(msg)))
# %% Read Lidarr's config file to get the base url and api key
with open('/config/config.xml') as f:
config = f.read()
lidarr_baseUrl = re.findall("<UrlBase>(.*)</UrlBase>", config)[0]
lidarr_port = re.findall("<Port>(.*)</Port>", config)[0]
lidarr_url = f"http://127.0.0.1:{lidarr_port}{lidarr_baseUrl}"
api_keys = {
"ops": os.environ.get('API_KEY_OPS'),
"red": os.environ.get('API_KEY_RED'),
"lidarr": re.findall("<ApiKey>(.*)</ApiKey>", config)[0],
}
for key, val in api_keys.items():
logging.info(f'{"Found" if val else "Missing"} API key for {key}')
# %% Set headers with lidarr's API key
headers = {
'Content-Type': 'application/json',
"X-Api-Key": api_keys["lidarr"],
}
# %% Check that the event type is correct and handle testing
if lidarr['eventtype'] == 'Test':
if parse_response(requests.get(f'{lidarr_url}/api', headers=headers)):
logging.info('Auto-beets successfully tested! Take a break and drink some tea!')
raise SystemExit()
else:
logging.error('Something is wrong! Take a break, make some tea, and come back if you feel up for it!')
raise Exception('Something is wrong!')
elif lidarr['eventtype'] != 'AlbumDownload':
logging.warning(f"You are running auto-beets on a {lidarr['eventtype']} event!"
f" I only know how to work with import/upgrade but I'll try my best!")
# %% Get info about the release
params = {
'artistId': lidarr['artist_id'],
'albumId': lidarr['album_id'],
'eventType': 'Grabbed'
}
albumHistory = parse_response(requests.get(f'{lidarr_url}/api/v1/history/artist', params=params, headers=headers))
if albumHistory:
torrent_URL = albumHistory[0]['data']['nzbInfoUrl']
else:
torrent_URL = []
logging.warning("Can't find the grab event in history!")
# If the hash isn't provided for some reason, look for it
if not lidarr['torrent_hash'] and albumHistory:
lidarr['torrent_hash'] = albumHistory[0]['downloadId']
elif not lidarr['torrent_hash']:
params['eventType'] = "trackFileImported"
albumHistory = parse_response(
requests.get(f'{lidarr_url}/api/v1/history/artist', params=params, headers=headers))
if albumHistory:
lidarr['torrent_hash'] = albumHistory[0]['downloadId']
# %% Get the folder where the album exists
if lidarr['addedtrackpaths']:
album_path = os.path.dirname(lidarr['addedtrackpaths'].split('|')[0])
else:
logging.warning("Didn't see lidarr['addedtrackpaths'], getting directory from the API")
params = {'albumId': lidarr['album_id']}
trackFile = parse_response(requests.get(f'{lidarr_url}/api/v1/trackFile', params=params, headers=headers))
album_path = os.path.dirname(trackFile[0]['path'])
logging.info(f"album path = {album_path}")
logging.info(f"torrent url = {torrent_URL}")
logging.info(f"torrent hash = {lidarr['torrent_hash']}")
# %% Download album data from gazelle
if lidarr['torrent_hash']:
if "redacted" in torrent_URL:
trackers = ["red"]
elif "orpheus" in torrent_URL:
trackers = ["ops"]
else:
trackers = [key for key in api_keys if key in ['red', 'ops'] and api_keys[key] is not None]
if trackers:
logging.warning(f"Couldn't find the tracker url, trying {' and '.join(trackers).upper()}")
for tracker in trackers:
origin_file = os.path.join(album_path, "origin-" + tracker + ".yaml")
if not os.path.isfile(origin_file):
logging.info(f"Looking for origin data on {tracker.upper()}")
cmd = ["gazelle-origin", "-o", origin_file, "--tracker", tracker, "--api-key", api_keys[tracker],
lidarr['torrent_hash']]
process = subprocess.run(cmd, capture_output=True, text=True)
if process.returncode:
logging.warning(f"gazelle-origin-{tracker.upper()}: {process.stderr.strip()}")
elif os.path.isfile(origin_file):
logging.info(f'Origin data saved at "{origin_file}"')
break
else:
logging.info(f"Origin data already exists at {origin_file}")
# %% Get the old release ID so we can check if the one beets find is different
if not lidarr['albumrelease_mbid']:
params = {'albumIds': lidarr['album_id']}
album = parse_response(requests.get(f'{lidarr_url}/api/v1/album', params=params, headers=headers))[0]
for release in album["releases"]:
if release["monitored"]:
lidarr['albumrelease_mbid'] = release['foreignReleaseId']
# %% Run beets!
cmd = ["beet", 'import', '-l', '/config/logs/beets.txt', '-q', '--write', '--flat', '--nocopy', album_path]
#cmd = ["beet", 'import', '-l', '/config/logs/beets.txt', '-q', album_path]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
env=dict(os.environ, BEETSDIR="/beets"))
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
with process.stdout:
try:
for line in iter(process.stdout.readline, b''):
logging.info(f'\t\t{ansi_escape.sub("", line.decode("utf-8").strip())}')
except subprocess.CalledProcessError as err:
logging.exception("Beets error!")
# %% Check if the release ID changed, so we can justify wasting time on this
if lidarr['albumrelease_mbid']:
cmd = ["beet", 'list', '-af', 'id=$mb_albumid', album_path]
process = subprocess.run(cmd, capture_output=True, text=True, env=dict(os.environ, BEETSDIR="/beets"))
musicbrainz_id = re.search('^id=(.+)$', process.stdout, re.M).group(1)
if musicbrainz_id != lidarr['albumrelease_mbid']:
logging.info(f"Updating lidarr release from https://musicbrainz.org/release/{lidarr['albumrelease_mbid']} "
f"to https://musicbrainz.org/release/{musicbrainz_id}")
else:
logging.info("The lidarr release ID already matches the one from beets, nice!")
logging.info(
"Finished! I hope your perfectionist soul is satisfied. Rest, you have earned it! And remember to listen, not just catalog!")Registered in Lidarr as the custom script; just invokes auto-beets.py.
#!/bin/bash
python /beets/beets-lidarr.py