From b861a1a8f4b42e42a8bc9d380ccf0abdb1be393b Mon Sep 17 00:00:00 2001 From: Kamil Date: Tue, 3 Dec 2024 23:11:05 +0000 Subject: [PATCH] feat: added lidarr support --- app/__init__.py | 24 +-- app/functions.py | 51 ++++- app/models.py | 1 + app/routes/routes.py | 70 +++---- app/tasks.py | 93 +++++++++- config.py | 4 + lidarr/__init__.py | 2 + lidarr/classes.py | 174 ++++++++++++++++++ lidarr/client.py | 143 ++++++++++++++ .../d13088ebddc5_add_lidarr_processed_flag.py | 32 ++++ templates/admin.html | 3 + templates/admin/lidarr.html | 32 ++++ 12 files changed, 570 insertions(+), 59 deletions(-) create mode 100644 lidarr/__init__.py create mode 100644 lidarr/classes.py create mode 100644 lidarr/client.py create mode 100644 migrations/versions/d13088ebddc5_add_lidarr_processed_flag.py create mode 100644 templates/admin/lidarr.html diff --git a/app/__init__.py b/app/__init__.py index 12e751e..03e226f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,6 +8,7 @@ from flask import Flask, has_request_context from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from psycopg2 import OperationalError +import redis import spotipy from spotipy.oauth2 import SpotifyClientCredentials from celery import Celery @@ -77,9 +78,13 @@ def make_celery(app): 'update_jellyfin_id_for_downloaded_tracks-schedule': { 'task': 'app.tasks.update_jellyfin_id_for_downloaded_tracks', 'schedule': crontab(minute='*/10'), - } } + if app.config['LIDARR_API_KEY']: + celery.conf.beat_schedule['request-lidarr-schedule'] = { + 'task': 'app.tasks.request_lidarr', + 'schedule': crontab(minute='*/15') + } celery.conf.timezone = 'UTC' return celery @@ -89,17 +94,6 @@ device_id = f'JellyPlist_{'_'.join(sys.argv)}' # Initialize Flask app app = Flask(__name__, template_folder="../templates", static_folder='../static') -# log_file = 'app.log' -# handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=3) -# handler.setLevel(logging.DEBUG) -# handler.setFormatter(log_formatter) -# stream_handler = logging.StreamHandler(sys.stdout) -# stream_handler.setLevel(logging.DEBUG) -# stream_handler.setFormatter(log_formatter) - - -# # app.logger.addHandler(handler) -# app.logger.addHandler(stream_handler) app.config.from_object(Config) @@ -114,6 +108,7 @@ logging.basicConfig(format=FORMAT) Config.validate_env_vars() cache = Cache(app) +redis_client = redis.StrictRedis(host=app.config['CACHE_REDIS_HOST'], port=app.config['CACHE_REDIS_PORT'], db=0, decode_responses=True) # Spotify, Jellyfin, and Spotdl setup @@ -181,3 +176,8 @@ spotify_client = SpotifyClient('/jellyplist/open.spotify.com_cookies.txt') spotify_client.authenticate() from .registry import MusicProviderRegistry MusicProviderRegistry.register_provider(spotify_client) + +if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: + app.logger.info(f'Creating Lidarr Client with URL: {app.config["LIDARR_URL"]}') + from lidarr.client import LidarrClient + lidarr_client = LidarrClient(app.config['LIDARR_URL'], app.config['LIDARR_API_KEY']) diff --git a/app/functions.py b/app/functions.py index 9317648..c188087 100644 --- a/app/functions.py +++ b/app/functions.py @@ -4,12 +4,13 @@ from flask import flash, redirect, session, url_for,g import requests from app.classes import CombinedPlaylistData, CombinedTrackData from app.models import JellyfinUser, Playlist,Track -from app import sp, cache, app, jellyfin ,jellyfin_admin_token, jellyfin_admin_id,device_id, cache +from app import sp, cache, app, jellyfin ,jellyfin_admin_token, jellyfin_admin_id,device_id, cache, redis_client from functools import wraps from celery.result import AsyncResult from app.providers import base from app.providers.base import PlaylistTrack from app.registry.music_provider_registry import MusicProviderRegistry +from lidarr.classes import Album, Artist from . import tasks from jellyfin.objects import PlaylistMetadata from spotipy.exceptions import SpotifyException @@ -20,15 +21,20 @@ TASK_STATUS = { 'update_all_playlists_track_status': None, 'download_missing_tracks': None, 'check_for_playlist_updates': None, - 'update_jellyfin_id_for_downloaded_tracks' : None + 'update_jellyfin_id_for_downloaded_tracks' : None, + + } +if app.config['LIDARR_API_KEY']: + TASK_STATUS['request_lidarr'] = None + LOCK_KEYS = [ 'update_all_playlists_track_status_lock', 'download_missing_tracks_lock', 'check_for_playlist_updates_lock', 'update_jellyfin_id_for_downloaded_tracks_lock' , - 'full_update_jellyfin_ids' - + 'full_update_jellyfin_ids', + 'request_lidarr_lock' ] def manage_task(task_name): @@ -46,6 +52,8 @@ def manage_task(task_name): result = tasks.check_for_playlist_updates.delay() elif task_name == 'update_jellyfin_id_for_downloaded_tracks': result = tasks.update_jellyfin_id_for_downloaded_tracks.delay() + elif task_name == 'request_lidarr': + result = tasks.request_lidarr.delay() TASK_STATUS[task_name] = result.id return result.state, result.info if result.info else {} @@ -93,6 +101,41 @@ def prepPlaylistData(playlist: base.Playlist) -> Optional[CombinedPlaylistData]: status=status ) +def lidarr_quality_profile_id(profile_id=None): + if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: + from app import lidarr_client + if profile_id: + redis_client.set('lidarr_quality_profile_id', profile_id) + else: + value = redis_client.get('lidarr_quality_profile_id') + if not value: + value = lidarr_client.get_quality_profiles()[0] + lidarr_quality_profile_id(value.id) + return value + return value + +def lidarr_root_folder_path(folder_path=None): + if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: + from app import lidarr_client + if folder_path: + redis_client.set('lidarr_root_folder_path', folder_path) + else: + value = redis_client.get('lidarr_root_folder_path') + if not value: + value = lidarr_client.get_root_folders()[0] + lidarr_root_folder_path(value.path) + return value.path + return value + +# a function which takes a lidarr.class.Artist object as a parameter, and applies the lidarr_quality_profile_id to the artist if its 0 +def apply_default_profile_and_root_folder(object : Artist ) -> Artist: + if object.qualityProfileId == 0: + object.qualityProfileId = int(lidarr_quality_profile_id()) + if object.rootFolderPath == '' or object.rootFolderPath == None: + object.rootFolderPath = str(lidarr_root_folder_path()) + if object.metadataProfileId == 0: + object.metadataProfileId = 1 + return object @cache.memoize(timeout=3600*24*10) def get_cached_provider_track(track_id : str,provider_id : str)-> base.Track: diff --git a/app/models.py b/app/models.py index da454e8..42f4dc4 100644 --- a/app/models.py +++ b/app/models.py @@ -61,5 +61,6 @@ class Track(db.Model): # Many-to-Many relationship with Playlists playlists = db.relationship('Playlist', secondary=playlist_tracks, back_populates='tracks') + lidarr_processed = db.Column(db.Boolean(), default=False) def __repr__(self): return f'' diff --git a/app/routes/routes.py b/app/routes/routes.py index 0ee8e3b..ef9da3d 100644 --- a/app/routes/routes.py +++ b/app/routes/routes.py @@ -1,8 +1,9 @@ +from dbm import error import json import os import re from flask import Flask, Response, jsonify, render_template, request, redirect, url_for, session, flash, Blueprint, g -from app import app, db, functions, sp, jellyfin, celery, jellyfin_admin_token, jellyfin_admin_id,device_id, cache, read_dev_build_file, tasks +from app import app, db, functions, jellyfin, read_dev_build_file, tasks from app.classes import AudioProfile, CombinedPlaylistData from app.models import JellyfinUser,Playlist,Track from celery.result import AsyncResult @@ -12,6 +13,8 @@ from app.providers import base from app.providers.base import MusicProviderClient from app.providers.spotify import SpotifyClient from app.registry.music_provider_registry import MusicProviderRegistry +from lidarr.classes import Album, Artist +from lidarr.client import LidarrClient from ..version import __version__ from spotipy.exceptions import SpotifyException from collections import defaultdict @@ -37,6 +40,29 @@ def render_messages(response: Response) -> Response: return response +@app.route('/admin/lidarr') +@functions.jellyfin_admin_required +def admin_lidarr(): + if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: + from app import lidarr_client + q_profiles = lidarr_client.get_quality_profiles() + root_folders = lidarr_client.get_root_folders() + return render_template('admin/lidarr.html',quality_profiles = q_profiles, root_folders = root_folders, current_quality_profile = functions.lidarr_quality_profile_id(), current_root_folder = functions.lidarr_root_folder_path()) + return render_template('admin/lidarr.html', error = 'Lidarr not configured') + +@app.route('/admin/lidarr/save', methods=['POST']) +@functions.jellyfin_admin_required +def save_lidarr_config(): + quality_profile_id = request.form.get('qualityProfile') + root_folder_id = request.form.get('rootFolder') + + if not quality_profile_id or not root_folder_id: + flash('Both Quality Profile and Root Folder must be selected', 'danger') + return redirect(url_for('admin_lidarr')) + functions.lidarr_quality_profile_id(quality_profile_id) + functions.lidarr_root_folder_path(root_folder_id) + flash('Configuration saved successfully', 'success') + return redirect(url_for('admin_lidarr')) @app.route('/admin/tasks') @functions.jellyfin_admin_required @@ -370,43 +396,5 @@ def unlock_key(): @pl_bp.route('/test') def test(): - #return '' - app.logger.info(f"performing full update on jellyfin track ids. (Update tracks and playlists if better quality will be found)") - downloaded_tracks : List[Track] = Track.query.all() - total_tracks = len(downloaded_tracks) - if not downloaded_tracks: - app.logger.info("No downloaded tracks without Jellyfin ID found.") - return {'status': 'No tracks to update'} - - app.logger.info(f"Found {total_tracks} tracks to update ") - processed_tracks = 0 - - for track in downloaded_tracks: - try: - best_match = tasks.find_best_match_from_jellyfin(track) - if best_match: - track.downloaded = True - if track.jellyfin_id != best_match['Id']: - track.jellyfin_id = best_match['Id'] - app.logger.info(f"Updated Jellyfin ID for track: {track.name} ({track.provider_track_id})") - if track.filesystem_path != best_match['Path']: - track.filesystem_path = best_match['Path'] - app.logger.info(f"Updated filesystem_path for track: {track.name} ({track.provider_track_id})") - - - - db.session.commit() - else: - app.logger.warning(f"No matching track found in Jellyfin for {track.name}.") - - spotify_track = None - - except Exception as e: - app.logger.error(f"Error searching Jellyfin for track {track.name}: {str(e)}") - - processed_tracks += 1 - progress = (processed_tracks / total_tracks) * 100 - #self.update_state(state=f'{processed_tracks}/{total_tracks}: {track.name}', meta={'current': processed_tracks, 'total': total_tracks, 'percent': progress}) - - app.logger.info("Finished updating Jellyfin IDs for all tracks.") - return {'status': 'All tracks updated', 'total': total_tracks, 'processed': processed_tracks} + return '' + \ No newline at end of file diff --git a/app/tasks.py b/app/tasks.py index f9c11c7..d9059d2 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -4,7 +4,7 @@ import subprocess from typing import List from sqlalchemy import insert -from app import celery, app, db, functions, sp, jellyfin, jellyfin_admin_token, jellyfin_admin_id +from app import celery, app, db, functions, sp, jellyfin, jellyfin_admin_token, jellyfin_admin_id, redis_client from app.classes import AudioProfile from app.models import JellyfinUser,Playlist,Track, user_playlists, playlist_tracks @@ -12,9 +12,10 @@ import os import redis from celery import current_task,signals +from app.providers import base from app.registry.music_provider_registry import MusicProviderRegistry +from lidarr.classes import Artist -redis_client = redis.StrictRedis(host='redis', port=6379, db=0) def acquire_lock(lock_name, expiration=60): return redis_client.set(lock_name, "locked", ex=expiration, nx=True) @@ -401,6 +402,94 @@ def update_jellyfin_id_for_downloaded_tracks(self): app.logger.info("Skipping task. Another instance is already running.") return {'status': 'Task skipped, another instance is running'} +@celery.task(bind=True) +def request_lidarr(self): + lock_key = "request_lidarr_lock" + + if acquire_lock(lock_key, expiration=600): + with app.app_context(): + if app.config['LIDARR_API_KEY'] and app.config['LIDARR_URL']: + from app import lidarr_client + try: + app.logger.info('Submitting request to Lidarr...') + # get all tracks from db + tracks = Track.query.filter_by(lidarr_processed=False).all() + total_items = len(tracks) + processed_items = 0 + for track in tracks: + tfp = functions.get_cached_provider_track(track.provider_track_id, provider_id=track.provider_id) + if tfp: + if app.config['LIDARR_MONITOR_ARTISTS']: + app.logger.debug("Monitoring artists instead of albums") + # get all artists from all tracks_from_provider and unique them + artists : dict[str,base.Artist] = {} + + for artist in tfp.artists: + artists[artist.name] = artist + app.logger.debug(f"Found {len(artists)} artists to monitor") + #pylint: disable=consider-using-dict-items + for artist in artists: + artist_from_lidarr = None + search_result = lidarr_client.search(artists[artist].name) + for url in artists[artist].external_urls: + artist_from_lidarr : Artist = lidarr_client.get_object_by_external_url(search_result, url.url) + if artist_from_lidarr: + app.logger.debug(f"Found artist {artist_from_lidarr.artistName} by external url {url.url}") + functions.apply_default_profile_and_root_folder(artist_from_lidarr) + try: + lidarr_client.monitor_artist(artist_from_lidarr) + track.lidarr_processed = True + db.session.commit() + except Exception as e: + app.logger.error(f"Error monitoring artist {artist_from_lidarr.artistName}: {str(e)}") + + if not artist_from_lidarr: + # if the artist isnt found by the external url, search by name + artist_from_lidarr = lidarr_client.get_artists_by_name(search_result, artists[artist].name) + for artist2 in artist_from_lidarr: + functions.apply_default_profile_and_root_folder(artist2) + try: + lidarr_client.monitor_artist(artist2) + track.lidarr_processed = True + db.session.commit() + except Exception as e: + app.logger.error(f"Error monitoring artist {artist2.artistName}: {str(e)}") + + processed_items += 1 + self.update_state(state=f'{processed_items}/{total_items}: {artist}', meta={'current': processed_items, 'total': total_items, 'percent': (processed_items / total_items) * 100}) + + else: + if tfp.album: + album_from_lidarr = None + search_result = lidarr_client.search(tfp.album.name) + # if the album isnt found by the external url, search by name + album_from_lidarr = lidarr_client.get_albums_by_name(search_result, tfp.album.name) + for album2 in album_from_lidarr: + functions.apply_default_profile_and_root_folder(album2.artist) + try: + lidarr_client.monitor_album(album2) + track.lidarr_processed = True + db.session.commit() + except Exception as e: + app.logger.error(f"Error monitoring album {album2.title}: {str(e)}") + processed_items += 1 + self.update_state(state=f'{processed_items}/{total_items}: {tfp.album.name}', meta={'current': processed_items, 'total': total_items, 'percent': (processed_items / total_items) * 100}) + + + app.logger.info(f'Requests sent to Lidarr. Total items: {total_items}') + return {'status': 'Request sent to Lidarr'} + finally: + release_lock(lock_key) + + else: + app.logger.info('Lidarr API key or URL not set. Skipping request.') + release_lock(lock_key) + + + else: + app.logger.info("Skipping task. Another instance is already running.") + return {'status': 'Task skipped, another instance is running'} + def find_best_match_from_jellyfin(track: Track): app.logger.debug(f"Trying to find best match from Jellyfin server for track: {track.name}") search_results = jellyfin.search_music_tracks(jellyfin_admin_token, functions.get_longest_substring(track.name)) diff --git a/config.py b/config.py index ca151e2..3eb6027 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,10 @@ class Config: SEARCH_JELLYFIN_BEFORE_DOWNLOAD = os.getenv('SEARCH_JELLYFIN_BEFORE_DOWNLOAD',"true").lower() == 'true' FIND_BEST_MATCH_USE_FFPROBE = os.getenv('FIND_BEST_MATCH_USE_FFPROBE','false').lower() == 'true' SPOTIFY_COUNTRY_CODE = os.getenv('SPOTIFY_COUNTRY_CODE','DE') + LIDARR_API_KEY = os.getenv('LIDARR_API_KEY','') + LIDARR_URL = os.getenv('LIDARR_URL','') + LIDARR_MONITOR_ARTISTS = os.getenv('LIDARR_MONITOR_ARTISTS','false').lower() == 'true' + # SpotDL specific configuration SPOTDL_CONFIG = { 'cookie_file': '/jellyplist/cookies.txt', diff --git a/lidarr/__init__.py b/lidarr/__init__.py new file mode 100644 index 0000000..2a984da --- /dev/null +++ b/lidarr/__init__.py @@ -0,0 +1,2 @@ +from .client import LidarrClient +__all__ = ["LidarrClient"] \ No newline at end of file diff --git a/lidarr/classes.py b/lidarr/classes.py new file mode 100644 index 0000000..940be03 --- /dev/null +++ b/lidarr/classes.py @@ -0,0 +1,174 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +@dataclass +class Image: + url: str + coverType: str + extension: str + remoteUrl: str + +@dataclass +class Link: + url: str + name: str + +@dataclass +class Ratings: + votes: int + value: float + +@dataclass +class AddOptions: + monitor: str + albumsToMonitor: List[str] + monitored: bool + searchForMissingAlbums: bool + +@dataclass +class Statistics: + albumCount: int + trackFileCount: int + trackCount: int + totalTrackCount: int + sizeOnDisk: int + percentOfTracks: float + +@dataclass +class Member: + name: str + instrument: str + images: List[Image] + +@dataclass +class Artist: + mbId: Optional[str] = None + tadbId: Optional[int] = None + discogsId: Optional[int] = None + allMusicId: Optional[str] = None + overview: str = "" + artistType: str = "" + disambiguation: str = "" + links: List[Link] = field(default_factory=list) + nextAlbum: str = "" + lastAlbum: str = "" + images: List[Image] = field(default_factory=list) + members: List[Member] = field(default_factory=list) + remotePoster: str = "" + path: str = "" + qualityProfileId: int = 0 + metadataProfileId: int = 0 + monitored: bool = False + monitorNewItems: str = "" + rootFolderPath: Optional[str] = None + folder: str = "" + genres: List[str] = field(default_factory=list) + cleanName: str = "" + sortName: str = "" + tags: List[int] = field(default_factory=list) + added: str = "" + addOptions: Optional[AddOptions] = None + ratings: Optional[Ratings] = None + statistics: Optional[Statistics] = None + status : str = "" + ended : bool = False + artistName : str = "" + foreignArtistId : str = "" + id : int = 0 + + +@dataclass +class Media: + mediumNumber: int + mediumName: str + mediumFormat: str + +@dataclass +class Release: + id: int + albumId: int + foreignReleaseId: str + title: str + status: str + duration: int + trackCount: int + media: List[Media] + mediumCount: int + disambiguation: str + country: List[str] + label: List[str] + format: str + monitored: bool + +@dataclass +class Album: + id: int = 0 + title: str = "" + disambiguation: str = "" + overview: str = "" + artistId: int = 0 + foreignAlbumId: str = "" + monitored: bool = False + anyReleaseOk: bool = False + profileId: int = 0 + duration: int = 0 + albumType: str = "" + secondaryTypes: List[str] = field(default_factory=list) + mediumCount: int = 0 + ratings: Ratings = None + releaseDate: str = "" + releases: List[Release] = field(default_factory=list) + genres: List[str] = field(default_factory=list) + media: List[Media] = field(default_factory=list) + artist: Artist = field(default_factory=Artist) + images: List[Image] = field(default_factory=list) + links: List[Link] = field(default_factory=list) + lastSearchTime: str = "" + statistics: Statistics = None + addOptions: Optional[dict] = field(default_factory=dict) + remoteCover: str = "" + +@dataclass +class RootFolder: + id: int = 0 + name: str = "" + path: str = "" + defaultMetadataProfileId: int = 0 + defaultQualityProfileId: int = 0 + defaultMonitorOption: str = "" + defaultNewItemMonitorOption: str = "" + defaultTags: List[int] = field(default_factory=list) + accessible: bool = False + freeSpace: int = 0 + totalSpace: int = 0 + +@dataclass +class Quality: + id: int = 0 + name: str = "" + +@dataclass +class Item: + id: int = 0 + name: str = "" + quality: Quality = field(default_factory=Quality) + items: List[str] = field(default_factory=list) + allowed: bool = False + +@dataclass +class FormatItem: + id: int = 0 + format: int = 0 + name: str = "" + score: int = 0 + +@dataclass +class QualityProfile: + id: int = 0 + name: str = "" + upgradeAllowed: bool = False + cutoff: int = 0 + items: List[Item] = field(default_factory=list) + minFormatScore: int = 0 + cutoffFormatScore: int = 0 + formatItems: List[FormatItem] = field(default_factory=list) \ No newline at end of file diff --git a/lidarr/client.py b/lidarr/client.py new file mode 100644 index 0000000..48feee2 --- /dev/null +++ b/lidarr/client.py @@ -0,0 +1,143 @@ +import json +import re +from flask import jsonify +import requests +from typing import List, Optional +from .classes import Album, Artist, QualityProfile, RootFolder +import logging +l = logging.getLogger(__name__) + +class LidarrClient: + def __init__(self, base_url: str, api_token: str): + self.base_url = base_url + self.api_token = api_token + self.headers = { + 'X-Api-Key': self.api_token + } + + def _get(self, endpoint: str, params: Optional[dict] = None): + response = requests.get(f"{self.base_url}{endpoint}", headers=self.headers, params=params) + response.raise_for_status() + return response.json() + def _post(self, endpoint: str, json: dict): + response = requests.post(f"{self.base_url}{endpoint}", headers=self.headers, json=json) + response.raise_for_status() + return response.json() + + def _put(self, endpoint: str, json: dict): + response = requests.put(f"{self.base_url}{endpoint}", headers=self.headers, json=json) + response.raise_for_status() + return response.json() + + def get_album(self, album_id: int) -> Album: + l.debug(f"Getting album {album_id}") + data = self._get(f"/api/v1/album/{album_id}") + return Album(**data) + + def get_artist(self, artist_id: int) -> Artist: + l.debug(f"Getting artist {artist_id}") + data = self._get(f"/api/v1/artist/{artist_id}") + return Artist(**data) + + def search(self, term: str) -> List[object]: + l.debug(f"Searching for {term}") + data = self._get("/api/v1/search", params={"term": term}) + results = [] + for item in data: + if 'artist' in item: + results.append(Artist(**item['artist'])) + elif 'album' in item: + results.append(Album(**item['album'])) + return results + # A method which takes a List[object] end external URL as parameter, and returns the object from the List[object] which has the same external URL as the parameter. + def get_object_by_external_url(self, objects: List[object], external_url: str) -> object: + l.debug(f"Getting object by external URL {external_url}") + # We need to check whether the external_url matches intl-[a-zA-Z]{2}\/ it has to be replaced by an empty string + external_url = re.sub(r"intl-[a-zA-Z]{2}\/", "", external_url) + for obj in objects: + # object can either be an Album or an Artist, so it can be verified and casted + if isinstance(obj, Album): + for link in obj.links: + if link['url'] == external_url: + return obj + elif isinstance(obj, Artist): + for link in obj.links: + if link['url'] == external_url: + return obj + + return None + # A method to get all Albums from List[object] where the name equals the parameter name + def get_albums_by_name(self, objects: List[object], name: str) -> List[Album]: + l.debug(f"Getting albums by name {name}") + albums = [] + for obj in objects: + if isinstance(obj, Album) and obj.title == name: + artist = Artist(**obj.artist) + obj.artist = artist + albums.append(obj) + return albums + + # a method to get all artists from List[object] where the name equals the parameter name + def get_artists_by_name(self, objects: List[object], name: str) -> List[Artist]: + l.debug(f"Getting artists by name {name}") + artists = [] + for obj in objects: + if isinstance(obj, Artist) and obj.artistName == name: + artists.append(obj) + return artists + + def create_album(self, album: Album) -> Album: + l.debug(f"Creating album {album.title}") + json_artist = album.artist.__dict__ + album.artist = json_artist + data = self._post("/api/v1/album", json=album.__dict__) + return Album(**data) + + def update_album(self, album_id: int, album: Album) -> Album: + l.debug(f"Updating album {album_id}") + json_artist = album.artist.__dict__ + album.artist = json_artist + data = self._put(f"/api/v1/album/{album_id}", json=album.__dict__) + return Album(**data) + + def create_artist(self, artist: Artist) -> Artist: + l.debug(f"Creating artist {artist.artistName}") + data = self._post("/api/v1/artist", json=artist.__dict__) + return Artist(**data) + + def update_artist(self, artist_id: int, artist: Artist) -> Artist: + l.debug(f"Updating artist {artist_id}") + data = self._put(f"/api/v1/artist/{artist_id}", json=artist.__dict__) + return Artist(**data) + + # shorthand method to set artist to monitored + def monitor_artist(self, artist: Artist): + artist.monitored = True + l.debug(f"Monitoring artist {artist.artistName}") + if artist.id == 0: + artist = self.create_artist(artist) + else: + self.update_artist(artist.id, artist) + # shorthand method to set album to monitored + def monitor_album(self, album: Album): + album.monitored = True + + l.debug(f"Monitoring album {album.title}") + if album.id == 0: + album = self.create_album(album) + else: + self.update_album(album.id, album) + + # a method to query /api/v1/rootfolder and return a List[RootFolder] + def get_root_folders(self) -> List[RootFolder]: + l.debug("Getting root folders") + data = self._get("/api/v1/rootfolder") + return [RootFolder(**folder) for folder in data] + + # a method to query /api/v1/qualityprofile and return a List[QualityProfile] + def get_quality_profiles(self) -> List[QualityProfile]: + l.debug("Getting quality profiles") + data = self._get("/api/v1/qualityprofile") + return [QualityProfile(**profile) for profile in data] + + \ No newline at end of file diff --git a/migrations/versions/d13088ebddc5_add_lidarr_processed_flag.py b/migrations/versions/d13088ebddc5_add_lidarr_processed_flag.py new file mode 100644 index 0000000..b8eb25b --- /dev/null +++ b/migrations/versions/d13088ebddc5_add_lidarr_processed_flag.py @@ -0,0 +1,32 @@ +"""Add lidarr_processed flag + +Revision ID: d13088ebddc5 +Revises: 18d056f49f59 +Create Date: 2024-12-03 22:44:21.287754 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd13088ebddc5' +down_revision = '18d056f49f59' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('track', schema=None) as batch_op: + batch_op.add_column(sa.Column('lidarr_processed', sa.Boolean(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('track', schema=None) as batch_op: + batch_op.drop_column('lidarr_processed') + + # ### end Alembic commands ### diff --git a/templates/admin.html b/templates/admin.html index d0164c4..7c55275 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -11,6 +11,9 @@ + diff --git a/templates/admin/lidarr.html b/templates/admin/lidarr.html new file mode 100644 index 0000000..d6a243c --- /dev/null +++ b/templates/admin/lidarr.html @@ -0,0 +1,32 @@ +{% extends "admin.html" %} + +{% block admin_content %} +
+

Lidarr Configuration

+ {% if error %} + + {% else %} +
+
+ + +
+
+ + +
+ +
+ {% endif %} +
+{% endblock %} \ No newline at end of file