Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Music Stack Documentation

Table of Contents


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

Lidarr Docker Compose

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-stopped

Lidarr Plugin: Tubifarry

github.com/TypNull/Tubifarry

Adds a bunch of features to Lidarr like Soulseek integration, YouTube Downloader, Queue Cleaner and Search Sniper.


Navidrome

Navidrome Docker Compose

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-stopped

Essentia Tagger (Mood Tags)

Looks for added files, analyzes the mood and writes them to the metadata.

https://github.com/WB2024/Essentia-to-Metadata/tree/main

Essentia systemd Service

[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

Beets

Beets config.yaml

# 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!"
###################################################################################################

auto-beets.py

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!")

Custom Script Wrapper

Registered in Lidarr as the custom script; just invokes auto-beets.py.

#!/bin/bash
python /beets/beets-lidarr.py

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors