updated logging on jellyfin api client
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user