updated logging on jellyfin api client

This commit is contained in:
Kamil
2024-11-24 21:39:40 +00:00
parent 3f5318a17b
commit 3e593bf475
2 changed files with 82 additions and 30 deletions

View File

@@ -8,7 +8,10 @@ import base64
from urllib.parse import quote from urllib.parse import quote
import acoustid import acoustid
import chromaprint import chromaprint
import logging
from app.routes import login
from jellyfin.objects import PlaylistMetadata from jellyfin.objects import PlaylistMetadata
def _clean_query(query): def _clean_query(query):
@@ -30,6 +33,9 @@ class JellyfinClient:
""" """
self.base_url = base_url self.base_url = base_url
self.timeout = timeout self.timeout = timeout
self.logger = logging.getLogger(self.__class__.__name__) # Logger named after the class
self.logger.setLevel(os.getenv('LOG_LEVEL', 'INFO').upper())
self.logger.debug(f"Initialized Jellyfin API Client. Base = '{self.base_url}', timeout = {timeout}")
def _get_headers(self, session_token: str): def _get_headers(self, session_token: str):
""" """
@@ -47,7 +53,7 @@ class JellyfinClient:
:param password: The password of the user. :param password: The password of the user.
:return: Access token and user ID :return: Access token and user ID
""" """
login_url = f'{self.base_url}/Users/AuthenticateByName' url = f'{self.base_url}/Users/AuthenticateByName'
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Emby-Authorization': f'MediaBrowser Client="JellyPlist", Device="Web", DeviceId="{device_id}", Version="1.0"' 'X-Emby-Authorization': f'MediaBrowser Client="JellyPlist", Device="Web", DeviceId="{device_id}", Version="1.0"'
@@ -56,9 +62,10 @@ class JellyfinClient:
'Username': username, 'Username': username,
'Pw': password 'Pw': password
} }
self.logger.debug(f"Url={url}")
response = requests.post(login_url, json=data, headers=headers) response = requests.post(url, json=data, headers=headers)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 200: if response.status_code == 200:
result = response.json() result = response.json()
return result['AccessToken'], result['User']['Id'], result['User']['Name'],result['User']['Policy']['IsAdministrator'] return result['AccessToken'], result['User']['Id'], result['User']['Name'],result['User']['Policy']['IsAdministrator']
@@ -74,7 +81,7 @@ class JellyfinClient:
:param song_ids: A list of song IDs to include in the playlist. :param song_ids: A list of song IDs to include in the playlist.
:return: The newly created playlist object :return: The newly created playlist object
""" """
create_url = f'{self.base_url}/Playlists' url = f'{self.base_url}/Playlists'
data = { data = {
'Name': name, 'Name': name,
'UserId': user_id, 'UserId': user_id,
@@ -82,8 +89,10 @@ class JellyfinClient:
'Ids': ','.join(song_ids), # Join song IDs with commas 'Ids': ','.join(song_ids), # Join song IDs with commas
'IsPublic' : False 'IsPublic' : False
} }
self.logger.debug(f"Url={url}")
response = requests.post(create_url, json=data, headers=self._get_headers(session_token=session_token), timeout = self.timeout) response = requests.post(url, json=data, headers=self._get_headers(session_token=session_token), timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
@@ -97,12 +106,14 @@ class JellyfinClient:
:param song_ids: A list of song IDs to include in the playlist. :param song_ids: A list of song IDs to include in the playlist.
:return: The updated playlist object :return: The updated playlist object
""" """
update_url = f'{self.base_url}/Playlists/{playlist_id}/Items' url = f'{self.base_url}/Playlists/{playlist_id}/Items'
data = { data = {
'Ids': ','.join(song_ids) # Join song IDs with commas 'Ids': ','.join(song_ids) # Join song IDs with commas
} }
self.logger.debug(f"Url={url}")
response = requests.post(update_url, json=data, headers=self._get_headers(session_token=session_token), timeout = self.timeout) response = requests.post(url, json=data, headers=self._get_headers(session_token=session_token), timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: # 204 No Content indicates success for updating if response.status_code == 204: # 204 No Content indicates success for updating
return {"status": "success", "message": "Playlist updated successfully"} return {"status": "success", "message": "Playlist updated successfully"}
@@ -110,11 +121,14 @@ class JellyfinClient:
raise Exception(f"Failed to update playlist: {response.content}") raise Exception(f"Failed to update playlist: {response.content}")
def get_playlist_metadata(self, session_token: str, user_id: str,playlist_id: str) -> PlaylistMetadata: def get_playlist_metadata(self, session_token: str, user_id: str,playlist_id: str) -> PlaylistMetadata:
playlist_metadata_url = f'{self.base_url}/Items/{playlist_id}' url = f'{self.base_url}/Items/{playlist_id}'
params = { params = {
'UserId' : user_id 'UserId' : user_id
} }
response = requests.get(playlist_metadata_url, headers=self._get_headers(session_token=session_token), timeout = self.timeout, params = params) self.logger.debug(f"Url={url}")
response = requests.get(url, headers=self._get_headers(session_token=session_token), timeout = self.timeout, params = params)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code != 200: if response.status_code != 200:
raise Exception(f"Failed to fetch playlist metadata: {response.content}") raise Exception(f"Failed to fetch playlist metadata: {response.content}")
@@ -145,8 +159,11 @@ class JellyfinClient:
setattr(metadata_obj, key, value) setattr(metadata_obj, key, value)
# Send the updated metadata to Jellyfin # Send the updated metadata to Jellyfin
update_url = f'{self.base_url}/Items/{playlist_id}' url = f'{self.base_url}/Items/{playlist_id}'
response = requests.post(update_url, json=metadata_obj.to_dict(), headers=self._get_headers(session_token= session_token), timeout = self.timeout, params = params) self.logger.debug(f"Url={url}")
response = requests.post(url, json=metadata_obj.to_dict(), headers=self._get_headers(session_token= session_token), timeout = self.timeout, params = params)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: if response.status_code == 204:
return {"status": "success", "message": "Playlist metadata updated successfully"} return {"status": "success", "message": "Playlist metadata updated successfully"}
@@ -159,14 +176,17 @@ class JellyfinClient:
Get all music playlists for the currently authenticated user. Get all music playlists for the currently authenticated user.
:return: A list of the user's music playlists :return: A list of the user's music playlists
""" """
playlists_url = f'{self.base_url}/Items' url = f'{self.base_url}/Items'
params = { params = {
'IncludeItemTypes': 'Playlist', # Retrieve only playlists 'IncludeItemTypes': 'Playlist', # Retrieve only playlists
'Recursive': 'true', # Include nested playlists 'Recursive': 'true', # Include nested playlists
'Fields': 'OpenAccess' # Fields we want 'Fields': 'OpenAccess' # Fields we want
} }
response = requests.get(playlists_url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout) self.logger.debug(f"Url={url}")
response = requests.get(url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 200: if response.status_code == 200:
return response.json()['Items'] return response.json()['Items']
@@ -178,7 +198,10 @@ class JellyfinClient:
params = { params = {
} }
self.logger.debug(f"Url={url}")
response = requests.get(url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout) response = requests.get(url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
@@ -195,7 +218,10 @@ class JellyfinClient:
"RegenerateTrickplay": "false", "RegenerateTrickplay": "false",
"ReplaceAllMetadata": "false" "ReplaceAllMetadata": "false"
} }
self.logger.debug(f"Url={url}")
response = requests.post(url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout) response = requests.post(url, headers=self._get_headers(session_token=session_token), params=params , timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: if response.status_code == 204:
return True return True
else: else:
@@ -209,7 +235,7 @@ class JellyfinClient:
:param search_query: The search term (title or song name). :param search_query: The search term (title or song name).
:return: A list of matching songs. :return: A list of matching songs.
""" """
search_url = f'{self.base_url}/Items' url = f'{self.base_url}/Items'
params = { params = {
'SearchTerm': search_query.replace('\'',"´").replace('','´'), 'SearchTerm': search_query.replace('\'',"´").replace('','´'),
@@ -217,8 +243,11 @@ class JellyfinClient:
'Recursive': 'true', # Search within all folders 'Recursive': 'true', # Search within all folders
'Fields': 'Name,Id,Album,Artists,Path' # Retrieve the name and ID of the song 'Fields': 'Name,Id,Album,Artists,Path' # Retrieve the name and ID of the song
} }
self.logger.debug(f"Url={url}")
response = requests.get(search_url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout) response = requests.get(url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 200: if response.status_code == 200:
return response.json()['Items'] return response.json()['Items']
@@ -233,14 +262,17 @@ class JellyfinClient:
:return: A success message. :return: A success message.
""" """
# Construct the API URL with query parameters # Construct the API URL with query parameters
add_url = f'{self.base_url}/Playlists/{playlist_id}/Items' url = f'{self.base_url}/Playlists/{playlist_id}/Items'
params = { params = {
'ids': ','.join(song_ids), # Comma-separated song IDs 'ids': ','.join(song_ids), # Comma-separated song IDs
'userId': user_id 'userId': user_id
} }
self.logger.debug(f"Url={url}")
# Send the request to Jellyfin API with query parameters # Send the request to Jellyfin API with query parameters
response = requests.post(add_url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout) response = requests.post(url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
# Check for success # Check for success
if response.status_code == 204: # 204 No Content indicates success if response.status_code == 204: # 204 No Content indicates success
@@ -255,12 +287,14 @@ class JellyfinClient:
:param song_ids: A list of song IDs to remove. :param song_ids: A list of song IDs to remove.
:return: A success message. :return: A success message.
""" """
remove_url = f'{self.base_url}/Playlists/{playlist_id}/Items' url = f'{self.base_url}/Playlists/{playlist_id}/Items'
params = { params = {
'EntryIds': ','.join(song_ids) # Join song IDs with commas 'EntryIds': ','.join(song_ids) # Join song IDs with commas
} }
self.logger.debug(f"Url={url}")
response = requests.delete(remove_url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout)
response = requests.delete(url, headers=self._get_headers(session_token=session_token), params=params, timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: # 204 No Content indicates success for updating if response.status_code == 204: # 204 No Content indicates success for updating
return {"status": "success", "message": "Songs removed from playlist successfully"} return {"status": "success", "message": "Songs removed from playlist successfully"}
@@ -273,9 +307,11 @@ class JellyfinClient:
:param playlist_id: The ID of the playlist to remove. :param playlist_id: The ID of the playlist to remove.
:return: A success message upon successful deletion. :return: A success message upon successful deletion.
""" """
remove_url = f'{self.base_url}/Items/{playlist_id}' url = f'{self.base_url}/Items/{playlist_id}'
self.logger.debug(f"Url={url}")
response = requests.delete(remove_url, headers=self._get_headers(session_token=session_token), timeout = self.timeout)
response = requests.delete(url, headers=self._get_headers(session_token=session_token), timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: # 204 No Content indicates successful deletion if response.status_code == 204: # 204 No Content indicates successful deletion
return {"status": "success", "message": "Playlist removed successfully"} return {"status": "success", "message": "Playlist removed successfully"}
@@ -293,9 +329,11 @@ class JellyfinClient:
""" """
# Construct the API endpoint URL # Construct the API endpoint URL
url = f'{self.base_url}/Playlists/{playlist_id}/Users/{user_id}' url = f'{self.base_url}/Playlists/{playlist_id}/Users/{user_id}'
self.logger.debug(f"Url={url}")
# Send the DELETE request to remove the user from the playlist # Send the DELETE request to remove the user from the playlist
response = requests.delete(url, headers=self._get_headers(session_token= session_token), timeout = self.timeout) response = requests.delete(url, headers=self._get_headers(session_token= session_token), timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if response.status_code == 204: if response.status_code == 204:
# 204 No Content indicates the user was successfully removed # 204 No Content indicates the user was successfully removed
@@ -335,11 +373,13 @@ class JellyfinClient:
headers['Content-Type'] = content_type # Set to the correct image type headers['Content-Type'] = content_type # Set to the correct image type
headers['Accept'] = '*/*' headers['Accept'] = '*/*'
# Step 5: Upload the Base64-encoded image to Jellyfin as a plain string in the request body # url 5: Upload the Base64-encoded image to Jellyfin as a plain string in the request body
upload_url = f'{self.base_url}/Items/{playlist_id}/Images/Primary' url = f'{self.base_url}/Items/{playlist_id}/Images/Primary'
self.logger.debug(f"Url={url}")
# Send the Base64-encoded image data # Send the Base64-encoded image data
upload_response = requests.post(upload_url, headers=headers, data=image_base64, timeout = self.timeout) upload_response = requests.post(url, headers=headers, data=image_base64, timeout = self.timeout)
self.logger.debug(f"Response = {response.status_code}")
if upload_response.status_code == 204: # 204 No Content indicates success if upload_response.status_code == 204: # 204 No Content indicates success
return {"status": "success", "message": "Playlist cover image updated successfully"} return {"status": "success", "message": "Playlist cover image updated successfully"}
@@ -418,20 +458,30 @@ class JellyfinClient:
""" """
try: try:
# Download the Spotify preview audio # Download the Spotify preview audio
self.logger.debug(f"Downloading preview {preview_url} to tmp file")
tmp = self.download_preview_to_tempfile(preview_url=preview_url) tmp = self.download_preview_to_tempfile(preview_url=preview_url)
if tmp is None: if tmp is None:
self.logger.error(f"Downloading preview {preview_url} to tmp file failed, not continuing")
return False, None return False, None
# Convert the preview file to a normalized WAV file # Convert the preview file to a normalized WAV file
self.logger.debug(f"Converting preview to WAV file")
tmp_wav = self.convert_to_wav(tmp) tmp_wav = self.convert_to_wav(tmp)
if tmp_wav is None: if tmp_wav is None:
self.logger.error(f"Converting preview to WAV failed, not continuing")
os.remove(tmp) os.remove(tmp)
return False, None return False, None
# Fingerprint the normalized preview WAV file # Fingerprint the normalized preview WAV file
self.logger.debug(f"Performing fingerprinting on preview {tmp_wav}")
_, tmp_fp = acoustid.fingerprint_file(tmp_wav) _, tmp_fp = acoustid.fingerprint_file(tmp_wav)
tmp_fp_dec, version = chromaprint.decode_fingerprint(tmp_fp) tmp_fp_dec, version = chromaprint.decode_fingerprint(tmp_fp)
tmp_fp_dec = np.array(tmp_fp_dec, dtype=np.uint32) tmp_fp_dec = np.array(tmp_fp_dec, dtype=np.uint32)
self.logger.debug(f"decoded fingerprint for preview: {tmp_fp_dec}")
# Search for matching tracks in Jellyfin using only the song name # Search for matching tracks in Jellyfin using only the song name
search_query = song_name # Only use the song name in the search query search_query = song_name # Only use the song name in the search query
@@ -529,12 +579,14 @@ class JellyfinClient:
] ]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0: if result.returncode != 0:
self.logger.error(f"Error converting to WAV, subprocess exitcode: {result.returncode}")
os.remove(output_file.name) os.remove(output_file.name)
return None return None
return output_file.name return output_file.name
except Exception as e: except Exception as e:
print(f"Error converting to WAV: {str(e)}") self.logger.error(f"Error converting to WAV: {str(e)}")
return None return None
def sliding_fingerprint_similarity(self, full_fp, preview_fp): def sliding_fingerprint_similarity(self, full_fp, preview_fp):

View File

@@ -13,8 +13,8 @@ spotdl==4.2.10
spotipy==2.24.0 spotipy==2.24.0
SQLAlchemy==2.0.35 SQLAlchemy==2.0.35
Unidecode==1.3.8 Unidecode==1.3.8
chromaprint
psycopg2-binary psycopg2-binary
eventlet eventlet
pydub pydub
fuzzywuzzy fuzzywuzzy
lidarr-py @ git+https://github.com/devopsarr/lidarr-py.git