chore: initial
This commit is contained in:
commit
6545d82095
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (emby-scripts)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/emby-scripts.iml" filepath="$PROJECT_DIR$/emby-scripts.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (emby-scripts)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"access_token": "BQCdJ8TcebCvCeo2VM75nwko_tshza2WqpKlhWK_EZufuP4NXOWEtaks1sUfnHlnJt258CbM2Hsx78XUNbu84_4u-uKEHgj51fPRJzAVg91O6MeSd-3ZpIUHbXLaNjnUVBS7ltPClDRHyCuaFaRW8BKlA_F-Nq-BrbPD4vgJnqboI3prb3BH4GTl1tfkInodNAfIog5oeReyHF8vy85KtrSugMZeSptlUInUd4Z-IZLqXg", "token_type": "Bearer", "expires_in": 3600, "scope": "playlist-read-private", "expires_at": 1715037521, "refresh_token": "AQAA4rKcyTFqaVctXWaxmE3d0LqLJu3ms91BNXrlJeYfy_1fYmCyQ3VyX6rO6K0O4iGyRVNWT8lcJB6MGRuC5rLXG85ZFB2XQtpcab6ECyKrIQNcMNU7AJj_eiZgfTwNtFk"}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
{"access_token": "BQCWbHB-kY3mUtocqK43wX8Ljl4wluFQnOIA9zCF3JlvKW0jtZHd65RJN_06wX5pRZdbrjeZfBQcE0--0o89f_lvI2tKiDjynhD4h44GruTSa4a8QFE9JkrdA27MdVJ5IlbPKoBQFpNOrYan6xmeTjme1YCARuF_tkfcy8E-9RcRKDAvH5eThu6Q41H-Rr4", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "AQBij3uDeV9G6Z71rKFATWpcHJLhggDPYabbTEuvebdSKEqRY_5VqS98wkx3u037d7nBlaTHIQpeSqkbbOmLdIut56TZ3Yiva56R4t6PV22LSY0wUdTYfwSjP_nWr_jP1wU", "scope": "playlist-read-private", "expires_at": 1715044235}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
from src.clients.emby_client import EmbyClient
|
||||
from src.config import Config
|
||||
|
||||
# Create an instance of the Emby client
|
||||
emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
||||
|
||||
# Define an array of TV show names
|
||||
tv_shows = ["The Office (US)", "Parks and Recreation", "Bob's Burgers"]
|
||||
|
||||
# Create a new playlist
|
||||
playlist_name = "Bob's Office Park"
|
||||
playlist_type = "shows"
|
||||
playlist = emby.create_playlist(playlist_name, playlist_type)
|
||||
|
||||
# Get all the episodes for all of the TV shows in the array
|
||||
all_episodes = {}
|
||||
for show_name in tv_shows:
|
||||
show = emby.search(show_name, "Series")[0] # Assuming the first search result is the desired show
|
||||
seasons = emby.get_seasons(show["Id"])
|
||||
|
||||
all_episodes[show_name] = []
|
||||
for season in seasons:
|
||||
episodes = emby.get_episodes(show["Id"], season["Id"])
|
||||
all_episodes[show_name].extend(episodes)
|
||||
|
||||
# Sort the episodes of each show by season and episode number
|
||||
for show_name in all_episodes:
|
||||
all_episodes[show_name].sort(key=lambda x: (x["ParentIndexNumber"], x["IndexNumber"]))
|
||||
|
||||
# Find the maximum number of episodes among all shows
|
||||
max_episodes = max(len(episodes) for episodes in all_episodes.values())
|
||||
|
||||
# Initialize episode counters for each show
|
||||
episode_counters = {show_name: 0 for show_name in tv_shows}
|
||||
|
||||
# Initialize a set to keep track of added episode IDs
|
||||
added_episode_ids = set()
|
||||
|
||||
# Loop over the episodes and add them sequentially
|
||||
for i in range(max_episodes):
|
||||
for show_name in tv_shows:
|
||||
if episode_counters[show_name] < len(all_episodes[show_name]):
|
||||
episode = all_episodes[show_name][episode_counters[show_name]]
|
||||
episode_id = episode["Id"]
|
||||
|
||||
if episode_id not in added_episode_ids:
|
||||
emby.add_item_to_playlist(playlist["Id"], episode_id)
|
||||
print(f"Added episode: {show_name} - S{episode['ParentIndexNumber']:02}E{episode['IndexNumber']:02}")
|
||||
added_episode_ids.add(episode_id)
|
||||
else:
|
||||
print(f"Skipped duplicate episode: {show_name} - S{episode['ParentIndexNumber']:02}E{episode['IndexNumber']:02}")
|
||||
|
||||
episode_counters[show_name] += 1
|
||||
else:
|
||||
print(f"No more episodes for {show_name}")
|
||||
|
||||
print("Playlist creation completed.")
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import spotipy
|
||||
from spotipy.oauth2 import SpotifyOAuth
|
||||
import requests
|
||||
import logging
|
||||
from fuzzywuzzy import fuzz
|
||||
import re
|
||||
from src.config import Config
|
||||
from src.utils.string import StringUtils
|
||||
from src.clients.emby_client import EmbyClient
|
||||
from src.clients.spotify_client import SpotifyClient
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
# Get the user's playlists from Spotify
|
||||
spot = SpotifyClient(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET, Config.SPOTIFY_REDIRECT_URI,
|
||||
Config.SPOTIFY_SCOPE)
|
||||
emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
||||
|
||||
playlists = spot.get_playlists()
|
||||
|
||||
# Iterate over each Spotify playlist
|
||||
for playlist in playlists["items"]:
|
||||
playlist_name = playlist["name"]
|
||||
playlist_id = playlist["id"]
|
||||
playlist_owner = playlist["owner"]["display_name"]
|
||||
logging.info(f"Processing Spotify playlist: {playlist_name} ({playlist_owner})")
|
||||
|
||||
emby_playlist_name = f"{playlist_name} ({playlist_owner})"
|
||||
|
||||
emby_playlist_search_results = emby.search(emby_playlist_name, 'Playlist')
|
||||
|
||||
if emby_playlist_search_results:
|
||||
# Delete the existing playlist
|
||||
existing_playlist_id = emby_playlist_search_results[0]["Id"]
|
||||
|
||||
for existing_playlist in emby_playlist_search_results:
|
||||
if (
|
||||
existing_playlist["Name"] == emby_playlist_name
|
||||
and existing_playlist["Type"] == "Playlist"
|
||||
):
|
||||
emby.delete_playlist(existing_playlist['Id'])
|
||||
logging.info(
|
||||
f"Deleted existing Emby playlist: {emby_playlist_name} (ID: {existing_playlist_id})"
|
||||
)
|
||||
|
||||
# Create a new playlist in Emby
|
||||
try:
|
||||
emby_playlist = emby.create_playlist(emby_playlist_name, 'Audio')
|
||||
logging.info(
|
||||
f"Created Emby playlist: {playlist_name} (ID: {emby_playlist["Id"]})"
|
||||
)
|
||||
except (requests.exceptions.RequestException, KeyError) as e:
|
||||
logging.error(f"Error creating Emby playlist: {playlist_name}")
|
||||
logging.error(f"Error message: {str(e)}")
|
||||
|
||||
# Get the tracks in the Spotify playlist
|
||||
tracks = spot.get_playlist_tracks(playlist_id)
|
||||
|
||||
logging.info(f"Processing {len(tracks)} tracks in Spotify playlist")
|
||||
# Iterate over each track in the Spotify playlist
|
||||
added_tracks = 0
|
||||
for track in tracks:
|
||||
track_name = track["track"]["name"]
|
||||
artist_name = track["track"]["artists"][0]["name"]
|
||||
album_name = track["track"]["album"]["name"]
|
||||
clean_track_name = StringUtils.remove_special_characters(track_name)
|
||||
clean_artist_name = StringUtils.remove_special_characters(artist_name)
|
||||
|
||||
emby_search_results = emby.search_for_track(track_name, artist_name)
|
||||
emby_search_results_cleaned = emby.search_for_track(
|
||||
clean_track_name, clean_artist_name
|
||||
)
|
||||
|
||||
if emby_search_results:
|
||||
found_match = False
|
||||
for result in emby_search_results:
|
||||
if found_match:
|
||||
break
|
||||
|
||||
emby_item_id = result["Id"]
|
||||
|
||||
logging.debug(f'Matching {track["track"]["name"]} with {result["Name"]}')
|
||||
if spot.match_song(track["track"], result):
|
||||
logging.debug(f"Matched track: {track_name}")
|
||||
try:
|
||||
emby.add_item_to_playlist(emby_playlist['Id'], emby_item_id)
|
||||
logging.info(
|
||||
f"Added '{track_name}' by {artist_name} to Emby playlist"
|
||||
)
|
||||
found_match = True
|
||||
added_tracks += 1
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.warning(
|
||||
f"Error adding track to Emby playlist: {track_name}"
|
||||
)
|
||||
logging.warning(f"Error message: {str(e)}")
|
||||
else:
|
||||
logging.warning(
|
||||
f"No match found for '{track_name}' by {artist_name} in Emby, failed match_song"
|
||||
)
|
||||
logging.warning(f"SPOTIFY: {track_name} by {artist_name}")
|
||||
logging.warning(
|
||||
f"EMBY: {result['Name']} by {result.get('Artists', [])}"
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f"No match found for '{track_name}' by {artist_name} in Emby"
|
||||
)
|
||||
logging.info("Added tracks: " + str(added_tracks))
|
||||
added_tracks = 0
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from src.clients.emby_client import EmbyClient
|
||||
from src.services.playlist_service import PlaylistService
|
||||
from src.config import Config
|
||||
|
||||
# Create an instance of the Emby client for the source user
|
||||
source_emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
||||
|
||||
# Create an array of target usernames
|
||||
target_usernames = ["Alyssa", "Dalton", "Laura", "Zuko", "Azula"]
|
||||
target_kids_usernames = ["Alyssa", "Azula", "Zuko"]
|
||||
|
||||
# Get all the playlists for the source user
|
||||
playlists = source_emby.get_playlists()
|
||||
kids_playlists = source_emby.get_tagged_playlist('Kids')
|
||||
|
||||
# Iterate over each playlist
|
||||
for playlist in playlists:
|
||||
# Iterate over the target usernames
|
||||
for target_username in target_usernames:
|
||||
# Check if the target username is in the playlist name
|
||||
if target_username in playlist["Name"]:
|
||||
try:
|
||||
# Create an instance of the Emby client for the target user
|
||||
target_emby = EmbyClient(Config.EMBY_URL, target_username, '')
|
||||
# Create an instance of the PlaylistService
|
||||
playlist_service = PlaylistService(source_emby, target_emby)
|
||||
|
||||
# Check if the playlist already exists for the target user
|
||||
existing_playlist = next((p for p in target_emby.get_playlists() if p["Name"] == playlist["Name"]), None)
|
||||
|
||||
if existing_playlist:
|
||||
# Delete the existing playlist
|
||||
target_emby.delete_playlist(existing_playlist["Id"])
|
||||
print(f"Existing playlist '{playlist['Name']}' deleted for user '{target_username}'.")
|
||||
|
||||
# Copy the playlist to the target user
|
||||
new_playlist = playlist_service.copy_playlist_by_usernames(Config.EMBY_USERNAME, target_username, playlist["Id"])
|
||||
print(f"Playlist '{playlist['Name']}' copied successfully to user '{target_username}'. New playlist ID: {new_playlist['Id']}")
|
||||
except ValueError as e:
|
||||
print(f"Error copying playlist '{playlist['Name']}' to user '{target_username}': {str(e)}")
|
||||
|
||||
# Check if the playlist has the tag 'Kids'
|
||||
for playlist in kids_playlists:
|
||||
# Iterate over the target kids usernames
|
||||
for target_username in target_kids_usernames:
|
||||
try:
|
||||
# Create an instance of the Emby client for the target user
|
||||
target_emby = EmbyClient(Config.EMBY_URL, target_username, '')
|
||||
# Create an instance of the PlaylistService
|
||||
playlist_service = PlaylistService(source_emby, target_emby)
|
||||
|
||||
# Check if the playlist already exists for the target user
|
||||
existing_playlist = next((p for p in target_emby.get_playlists() if p["Name"] == playlist["Name"]), None)
|
||||
|
||||
if existing_playlist:
|
||||
# Delete the existing playlist
|
||||
target_emby.delete_playlist(existing_playlist["Id"])
|
||||
print(f"Existing playlist '{playlist['Name']}' deleted for user '{target_username}'.")
|
||||
|
||||
# Copy the playlist to the target user
|
||||
new_playlist = playlist_service.copy_playlist_by_usernames(Config.EMBY_USERNAME, target_username, playlist["Id"])
|
||||
print(f"Kids playlist '{playlist['Name']}' copied successfully to user '{target_username}'. New playlist ID: {new_playlist['Id']}")
|
||||
except ValueError as e:
|
||||
print(f"Error copying kids playlist '{playlist['Name']}' to user '{target_username}': {str(e)}")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,712 @@
|
|||
import logging
|
||||
import random
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import requests
|
||||
from requests.exceptions import Timeout
|
||||
import time
|
||||
from enum import Enum
|
||||
from src.config import Config
|
||||
|
||||
|
||||
class EmbyLibraryItemType(Enum):
|
||||
AUDIO = "Audio"
|
||||
VIDEO = "Video"
|
||||
FOLDER = "Folder"
|
||||
EPISODE = "Episode"
|
||||
MOVIE = "Movie"
|
||||
TRAILER = "Trailer"
|
||||
ADULT_VIDEO = "AdultVideo"
|
||||
MUSIC_VIDEO = "MusicVideo"
|
||||
BOX_SET = "BoxSet"
|
||||
MUSIC_ALBUM = "MusicAlbum"
|
||||
MUSIC_ARTIST = "MusicArtist"
|
||||
SEASON = "Season"
|
||||
SERIES = "Series"
|
||||
GAME = "Game"
|
||||
GAME_SYSTEM = "GameSystem"
|
||||
BOOK = "Book"
|
||||
|
||||
|
||||
class EmbyImageType(Enum):
|
||||
PRIMARY = "Primary"
|
||||
ART = "Art"
|
||||
BACKDROP = "Backdrop"
|
||||
BANNER = "Banner"
|
||||
LOGO = "Logo"
|
||||
THUMB = "Thumb"
|
||||
DISC = "Disc"
|
||||
BOX = "Box"
|
||||
SCREENSHOT = "Screenshot"
|
||||
MENU = "Menu"
|
||||
CHAPTER = "Chapter"
|
||||
|
||||
|
||||
class EmbyClient:
|
||||
def __init__(self, server_url, username, password):
|
||||
self.server_url = server_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
emby_auth_url = f"{server_url}/Users/AuthenticateByName"
|
||||
emby_auth_data = {"username": username, "pw": password}
|
||||
emby_auth_headers = {
|
||||
"Authorization": f'Emby UserId="{username}", Client="{Config.EMBY_CLIENT}", Device="{Config.EMBY_DEVICE}", DeviceId="{Config.EMBY_DEVICE_ID}", Version="{Config.EMBY_VERSION}"',
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
emby_auth_response = requests.post(
|
||||
emby_auth_url, json=emby_auth_data, headers=emby_auth_headers
|
||||
)
|
||||
|
||||
self.user = emby_auth_response.json()["User"]
|
||||
|
||||
self.headers = {
|
||||
"Authorization": f'Emby UserId="{username}", Client="{Config.EMBY_CLIENT}", Device="{Config.EMBY_DEVICE}", DeviceId="{Config.EMBY_DEVICE_ID}", Version="{Config.EMBY_VERSION}"',
|
||||
"X-Emby-Token": emby_auth_response.json()["AccessToken"],
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# self.user = self.get_user_by_username(username)
|
||||
self.user_id = self.user['Id']
|
||||
|
||||
def _build_url(self, path, params=None):
|
||||
url = f'{self.server_url}/emby/{path}'
|
||||
if params:
|
||||
url += '?' + '&'.join(f'{key}={value}' for key, value in params.items())
|
||||
return url
|
||||
|
||||
def _get_request_with_retry(self, url, retries=6, delay=1, stream=False):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, timeout=61, stream=stream)
|
||||
response.raise_for_status() # Raise an exception for non-1xx status codes
|
||||
if stream is True:
|
||||
return response
|
||||
else:
|
||||
return response.json()
|
||||
except (Timeout, requests.exceptions.RequestException, requests.exceptions.ReadTimeout) as e:
|
||||
print(f"Request failed: {e}")
|
||||
if attempt < retries - 2:
|
||||
print(f"Retrying in {delay} seconds...")
|
||||
time.sleep(delay)
|
||||
raise Exception(f"Failed to make the request after {retries} attempts.")
|
||||
|
||||
def _post_request_with_retry(self, url, data=None, files=None, retries=6, delay=1):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.post(url, data=data, files=files, headers=self.headers, timeout=61)
|
||||
response.raise_for_status() # Raise an exception for non-1xx status codes
|
||||
return response
|
||||
except (Timeout, requests.exceptions.RequestException, requests.exceptions.ReadTimeout) as e:
|
||||
print(f"Request failed: {e}")
|
||||
if attempt < retries - 2:
|
||||
print(f"Retrying in {delay} seconds...")
|
||||
time.sleep(delay)
|
||||
raise Exception(f"Failed to make the request after {retries} attempts.")
|
||||
|
||||
# Modify your existing methods to use the new _get_request_with_retry and _post_request_with_retry methods
|
||||
|
||||
def _delete_request_with_retry(self, url, retries=6, delay=1):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.delete(url, headers=self.headers, timeout=61)
|
||||
response.raise_for_status() # Raise an exception for non-1xx status codes
|
||||
return response
|
||||
except (Timeout, requests.exceptions.RequestException, requests.exceptions.ReadTimeout) as e:
|
||||
print(f"Request failed: {e}")
|
||||
if attempt < retries - 2:
|
||||
print(f"Retrying in {delay} seconds...")
|
||||
time.sleep(delay)
|
||||
raise Exception(f"Failed to make the request after {retries} attempts.")
|
||||
|
||||
def _get_request(self, url, stream=False, retries=6, delay=1):
|
||||
return self._get_request_with_retry(url, stream=stream, retries=retries, delay=delay)
|
||||
|
||||
def _post_request(self, url, data=None, files=None):
|
||||
return self._post_request_with_retry(url, data=data, files=files)
|
||||
|
||||
def _delete_request(self, url, retries=6, delay=1):
|
||||
return self._delete_request_with_retry(url, retries=retries, delay=delay)
|
||||
|
||||
def create_collection(self, name, type, sort_name=None, poster=None):
|
||||
|
||||
# Get the first items id of the correct type (so the collection is sorted right)
|
||||
initial_item_id = self.get_items_by_type(type, 2)[0]['Id']
|
||||
|
||||
url = self._build_url('Collections', {'Name': name, 'Ids': initial_item_id, 'userId': self.user_id})
|
||||
response = self._post_request(url)
|
||||
collection = response.json()
|
||||
|
||||
# TODO: Add sort name if other than None
|
||||
|
||||
print(f"Created collection: {collection['Name']} ({collection['Id']})")
|
||||
|
||||
if sort_name:
|
||||
self.update_item_sort_name(collection['Id'], sort_name)
|
||||
|
||||
# Remove the initial item from the collection,
|
||||
# since we don't want the item and I had errors trying
|
||||
# to create a collection without an initial item.
|
||||
|
||||
try:
|
||||
self.delete_item_from_collection(collection['Id'], initial_item_id)
|
||||
except:
|
||||
print(f"Failed to remove initial item #{initial_item_id} from collection")
|
||||
|
||||
return collection
|
||||
|
||||
def create_playlist(self, name, type, user_id=None, sort_name=None, poster=None):
|
||||
|
||||
if user_id is None:
|
||||
user_id = self.user_id
|
||||
# Get the first items id of the correct type (so the collection is sorted right)
|
||||
# initial_item_id = self.get_items_by_type(type, 2)[0]['Id']
|
||||
|
||||
url = self._build_url('Playlists', {'Name': name, 'userId': user_id})
|
||||
response = self._post_request(url)
|
||||
playlist = response.json()
|
||||
|
||||
print(f"Created playlist: {playlist['Name']} ({playlist['Id']})")
|
||||
|
||||
return playlist
|
||||
|
||||
def update_item_sort_name(self, item_id, sort_name):
|
||||
emby_watchlist_metadata = self.get_item_metadata(item_id)
|
||||
|
||||
emby_watchlist_metadata['ForcedSortName'] = sort_name
|
||||
emby_watchlist_metadata['SortName'] = sort_name
|
||||
emby_watchlist_metadata['LockedFields'] = ['SortName']
|
||||
|
||||
self.update_item_metadata(emby_watchlist_metadata)
|
||||
|
||||
def get_collections(self):
|
||||
url = self._build_url(f'users/{self.user_id}/items',
|
||||
{'Fields': 'ChildCount,RecursiveItemCount',
|
||||
'Recursive': 'true',
|
||||
'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'IncludeItemTypes': 'boxset'})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def get_playlists(self):
|
||||
url = self._build_url(f'users/{self.user_id}/items',
|
||||
{'Fields': 'ChildCount,RecursiveItemCount,Taglines',
|
||||
'Recursive': 'true',
|
||||
'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'IncludeItemTypes': 'playlist'})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def get_tagged_playlist(self, tag):
|
||||
url = self._build_url(f'users/{self.user_id}/items',
|
||||
{'Fields': 'ChildCount,RecursiveItemCount,Taglines',
|
||||
'Recursive': 'true',
|
||||
'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'Tags': tag,
|
||||
'IncludeItemTypes': 'playlist'})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
|
||||
def get_collection_by_name(self, name, item_type=None):
|
||||
collections = self.get_collections()
|
||||
|
||||
if item_type is not None:
|
||||
return next((item for item in collections if item.get('Type') == name and item.get('Name') == name), None)
|
||||
return next((item for item in collections if item.get('Name') == name), None)
|
||||
|
||||
def get_collection(self, collection_id):
|
||||
return self.get_list(collection_id)
|
||||
|
||||
def get_list(self, list_id):
|
||||
url = self._build_url(f'users/{self.user_id}/items/{list_id}')
|
||||
response = self._get_request(url)
|
||||
return response
|
||||
|
||||
def get_collection_items(self, collection_id):
|
||||
return self.get_list_items(collection_id)
|
||||
|
||||
def get_items_from_parent(self, parent_id, image_type_limit=2,
|
||||
fields='BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,ExternalUrls,Status,EndDate,ProviderIds',
|
||||
enable_total_record_count=True,
|
||||
limit: int = 51, offset: int = 0):
|
||||
params = {
|
||||
'ParentId': parent_id,
|
||||
'ImageTypeLimit': image_type_limit,
|
||||
'Fields': fields,
|
||||
'IncludeItemTypes': 'Movie,Series,Season,Episode',
|
||||
'Recursive': 'true',
|
||||
'EnableTotalRecordCount': enable_total_record_count,
|
||||
'StartIndex': offset,
|
||||
'Limit': limit
|
||||
}
|
||||
|
||||
url = self._build_url(f'Users/{self.user_id}/Items', params=params)
|
||||
response = self._get_request(url)
|
||||
items = response.get('Items', [])
|
||||
total_count = response.get('TotalRecordCount', 1)
|
||||
# print(items, total_count)
|
||||
return items, total_count
|
||||
|
||||
def get_libraries(self):
|
||||
url = self._build_url(f'Users/{self.user_id}/views')
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def get_library(self, library_id):
|
||||
libraries = self.get_libraries()
|
||||
library = next((item for item in libraries if item.get('Id') == library_id), None)
|
||||
return library
|
||||
|
||||
def get_items_from_library(self, library_name):
|
||||
libraries = self.get_libraries()
|
||||
library = next((item for item in libraries if item.get('Name') == library_name), None)
|
||||
if library:
|
||||
return self.get_items_from_parent(library['Id'])
|
||||
return None, 1
|
||||
|
||||
def get_list_items(self, list_id):
|
||||
url = self._build_url(f'Users/{self.user_id}/Items', {
|
||||
'ParentId': list_id,
|
||||
})
|
||||
response = self._get_request(url)
|
||||
items = response.get('Items', [])
|
||||
total_count = response.get('TotalRecordCount', 0)
|
||||
print(f'Found {total_count} items in playlist')
|
||||
return items, total_count
|
||||
|
||||
def get_seasons(self, series_id):
|
||||
print(f"Getting seasons for series {series_id}")
|
||||
url = self._build_url(f'Shows/{series_id}/Seasons')
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def get_episodes(self, series_id, season_id):
|
||||
print(f"Getting episodes for series {series_id} season {season_id}")
|
||||
url = self._build_url(f'Shows/{series_id}/Episodes', {'SeasonId': season_id})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def does_collection_exist(self, collection_name):
|
||||
collections = self.get_collections()
|
||||
for collection in collections:
|
||||
if collection.get('Name') == collection_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_collection_poster(self, collection_id):
|
||||
url = self._build_url(f'Items/{collection_id}/Images/Primary')
|
||||
response = requests.get(url, headers=self.headers)
|
||||
return response
|
||||
|
||||
def add_item_to_collection(self, collection_id, item_id):
|
||||
url = self._build_url(f'Collections/{collection_id}/Items', {'Ids': item_id})
|
||||
response = self._post_request(url)
|
||||
return response
|
||||
|
||||
def add_item_to_playlist(self, playlist_id, item_id):
|
||||
url = self._build_url(f'Playlists/{playlist_id}/Items', {'Ids': item_id})
|
||||
response = self._post_request(url)
|
||||
return response
|
||||
|
||||
def delete_item_from_collection(self, collection_id, item_id):
|
||||
url = self._build_url(f'Collections/{collection_id}/Items', {'Ids': item_id})
|
||||
response = self._delete_request(url)
|
||||
return response
|
||||
|
||||
def delete_item_from_playlist(self, playlist_id, item_id):
|
||||
url = self._build_url(f'Playlists/{playlist_id}/Items', {'EntryIds': item_id})
|
||||
response = self._delete_request(url)
|
||||
return response
|
||||
|
||||
def get_item_image(self, item_id):
|
||||
url = self._build_url(f'Items/{item_id}/Images/Primary')
|
||||
print(url)
|
||||
response = self._get_request(url, stream=True, retries=1)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Assuming _get_request is using the requests library.
|
||||
# Use BytesIO to convert the response content into a file-like object so it can be opened by PIL
|
||||
img = Image.open(BytesIO(response.content)).convert('RGBA')
|
||||
return img
|
||||
else:
|
||||
raise Exception(f"Failed to fetch the image for collection {item_id}. Status code: {response.status_code}")
|
||||
|
||||
def delete_collection(self, collection_id):
|
||||
return self.delete_item(collection_id)
|
||||
|
||||
def delete_playlist(self, playlist_id):
|
||||
return self.delete_item(playlist_id)
|
||||
|
||||
def delete_item(self, item_id):
|
||||
url = self._build_url(f'Items/{item_id}')
|
||||
response = self._delete_request(url)
|
||||
return response
|
||||
|
||||
def delete_all_collections(self):
|
||||
collections = self.get_collections()
|
||||
for collection in collections:
|
||||
# Skip this ALWAYS
|
||||
# TODO: implement some sort of "skip" list
|
||||
if (collection.get('Name') == 'Watchlist'):
|
||||
continue
|
||||
|
||||
print(f"Deleting collection {collection.get('Name')} ({collection.get('Id')})")
|
||||
self.delete_collection(collection.get('Id'))
|
||||
return
|
||||
|
||||
def delete_collection_by_name(self, collection_name):
|
||||
collection = self.get_collection_by_name(collection_name)
|
||||
if collection:
|
||||
self.delete_item(collection.get('Id'))
|
||||
return
|
||||
|
||||
def add_search_results_to_collection(self, collection_id, results):
|
||||
for item in results.get('Items', []):
|
||||
item_id = item.get('Id')
|
||||
print(f"Found {item.get('Name')} with id {item_id}")
|
||||
self.add_item_to_collection(collection_id, item_id)
|
||||
print(f"Added {item.get('Name')} to {collection_id}")
|
||||
|
||||
def delete_search_results_from_collection(self, collection_id, results):
|
||||
for item in results.get('Items', []):
|
||||
item_id = item.get('Id')
|
||||
print(f"Found {item.get('Name')} with id {item_id}")
|
||||
self.delete_item_from_collection(collection_id, item_id)
|
||||
print(f"Removed {item.get('Name')} from {collection_id}")
|
||||
|
||||
def get_items_by_type(self, item_types='Series', limit=50):
|
||||
url = self._build_url(f'Users/{self.user_id}/Items',
|
||||
{'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'IncludeItemTypes': item_types,
|
||||
'Recursive': 'true',
|
||||
'Fields': 'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,Prefix',
|
||||
'StartIndex': '0',
|
||||
'EnableImageTypes': 'Primary,Backdrop,Thumb',
|
||||
'ImageTypeLimit': '1',
|
||||
'Limit': limit})
|
||||
response = self._get_request_with_retry(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
def get_items_in_collection(self, collection_id):
|
||||
url = self._build_url(f'users/{self.user_id}/items', {'Parentid': collection_id})
|
||||
response = self._get_request(url)
|
||||
items = response.get('Items', [])
|
||||
return items, len(items)
|
||||
|
||||
def get_all_trailers(self):
|
||||
"""
|
||||
Retrieves all trailers from the Emby collection.
|
||||
"""
|
||||
item_type = 'Trailer'
|
||||
url = self._build_url(f'Users/{self.user_id}/Items',
|
||||
{
|
||||
'IncludeItemTypes': item_type,
|
||||
'Recursive': 'true',
|
||||
'Fields': 'Title,Year,Type,Description'
|
||||
})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
# def get_all_trailers(self):
|
||||
# url = self._build_url(f'Users/{self.user_id}/Items', {'IncludeItemTypes': 'Trailer'})
|
||||
# response = self._get_request(url)
|
||||
# return response.get('Items', [])
|
||||
|
||||
# def upload_image(self, id, image_path, imgType='Primary'):
|
||||
# mime_type = guess_type(image_path)[0]
|
||||
# with open(image_path, 'rb') as f:
|
||||
# image_data = f.read()
|
||||
# encoded_image_data = base64.b64encode(image_data)
|
||||
# headers = {'Content-Type': mime_type}
|
||||
# print('Uploading collection image: ', image_path, mime_type)
|
||||
# url = self._build_url(f'Items/{id}/Images/{imgType}')
|
||||
# response = requests.post(url, data=encoded_image_data, headers=headers)
|
||||
# return response
|
||||
|
||||
def get_item_metadata(self, item_id):
|
||||
url = self._build_url(f'Users/{self.user_id}/Items/{item_id}', {'Fields': 'ChannelMappingInfo'})
|
||||
response = self._get_request(url)
|
||||
return response
|
||||
|
||||
def update_item_metadata(self, metadata):
|
||||
url = self._build_url(f'Items/{metadata["Id"]}')
|
||||
response = requests.post(url, json=metadata)
|
||||
return response.text
|
||||
|
||||
def get_user_by_username(self, username):
|
||||
users = self.get_users()
|
||||
print(users)
|
||||
return next((user for user in users if user.get('Name') == username), None)
|
||||
|
||||
def set_favorite(self, item_id):
|
||||
url = self._build_url(f'Users/{self.user_id}/FavoriteItems/{item_id}')
|
||||
response = self._post_request(url)
|
||||
return response
|
||||
|
||||
def get_users(self):
|
||||
# https://emby.faiyts.media/emby/users/public?X-Emby-Client=Emby%20Web&X-Emby-Device-Name=Google%20Chrome%20Linux&X-Emby-Device-Id=ea453a6f-4ba4-4901-a3c5-dd875239c834&X-Emby-Client-Version=4.7.13.0&X-Emby-Language=en-us
|
||||
# url = self._build_url(f'Users')
|
||||
headers = {
|
||||
"X-Emby-Token": Config.EMBY_API_KEY,
|
||||
"X-Emby-Client": Config.EMBY_CLIENT,
|
||||
"X-Emby-Device-Name": Config.EMBY_DEVICE,
|
||||
"X-Emby-Device-Id": Config.EMBY_DEVICE_ID,
|
||||
"X-Emby-Client-Version": Config.EMBY_VERSION
|
||||
}
|
||||
|
||||
response = requests.get(f"{Config.EMBY_URL}/emby/Users", headers=headers)
|
||||
return response.json()
|
||||
|
||||
def search(self, query, item_type):
|
||||
url = self._build_url(f'Users/{self.user_id}/Items',
|
||||
{'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'IncludeItemTypes': item_type,
|
||||
'Fields': 'BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,Status,EndDate',
|
||||
'StartIndex': '0',
|
||||
'EnableImageTypes': 'Primary,Backdrop,Thumb',
|
||||
'ImageTypeLimit': '1',
|
||||
'Recursive': 'true',
|
||||
'SearchTerm': query,
|
||||
'Limit': '50',
|
||||
'IncludeSearchTypes': 'false'})
|
||||
response = self._get_request(url)
|
||||
return response.get('Items', [])
|
||||
|
||||
|
||||
def playlist_search(self, query, item_type):
|
||||
url = self._build_url(f'Users/{self.user_id}/Items',{
|
||||
'Fields': 'BasicSyncInfo,CanDelete,PrimaryImageAspectRatio,ProductionYear,Status,EndDate',
|
||||
'StartIndex': '0',
|
||||
'SortBy': 'SortName',
|
||||
'SortOrder': 'Ascending',
|
||||
'EnableImageTypes': 'Primary,Backdrop,Thumb',
|
||||
'ImageTypeLimit': '1',
|
||||
'Recursive': 'true',
|
||||
'SearchTerm': query,
|
||||
'GroupProgramsBySeries': 'true',
|
||||
'Limit': '50',
|
||||
})
|
||||
|
||||
# @staticmethod
|
||||
# def build_query_parameters(filter_data: EmbyFilters) -> Dict[str, Any]:
|
||||
# filter_dict = filter_data.dict(exclude_none=True)
|
||||
#
|
||||
# # Remap keys as necessary
|
||||
# remapped_keys = {
|
||||
# 'search': 'SearchTerm',
|
||||
# 'limit': 'Limit',
|
||||
# 'listId': 'ParentId',
|
||||
# 'offset': 'StartIndex',
|
||||
# # ... add other remapped keys here ...
|
||||
# }
|
||||
#
|
||||
# for key, new_key in remapped_keys.items():
|
||||
# if key in filter_dict:
|
||||
# filter_dict[new_key] = filter_dict.pop(key)
|
||||
#
|
||||
# # Special handling for Filters
|
||||
# filters_list = []
|
||||
#
|
||||
# if 'isPlayed' in filter_dict:
|
||||
# filters_list.append('IsUnplayed' if not filter_dict['isPlayed'] else 'IsPlayed')
|
||||
# del filter_dict['isPlayed']
|
||||
#
|
||||
# if 'isFavorite' in filter_dict and filter_dict['isFavorite']:
|
||||
# filters_list.append('IsFavorite')
|
||||
# del filter_dict['isFavorite']
|
||||
#
|
||||
# if filters_list:
|
||||
# filter_dict['Filters'] = ','.join(filters_list)
|
||||
#
|
||||
# # Capitalizing the first letter of each key in the dictionary
|
||||
# filter_dict = {key.capitalize(): value for key, value in filter_dict.items()}
|
||||
#
|
||||
# return filter_dict
|
||||
#
|
||||
# def filter_search(self, filters: EmbyFilters) -> Dict[str, Any]:
|
||||
# # Building the request URL
|
||||
# url = self._build_url(f'Users/{self.user_id}/Items', self.build_query_parameters(filters))
|
||||
#
|
||||
# response = self._get_request(url)
|
||||
# return response.get('Items', [])
|
||||
|
||||
def search_for_track(self, track_name, artist_name):
|
||||
emby_search_results = None
|
||||
url = self._build_url(
|
||||
f"Items?SearchTerm={track_name}&Artists={artist_name}&Recursive=true&IncludeItemTypes=Audio&ExcludeItemTypes=Podcast&Limit=10")
|
||||
try:
|
||||
emby_search_response = self._get_request(url)
|
||||
# print(emby_search_response)
|
||||
# emby_search_response.raise_for_status()
|
||||
emby_search_results = emby_search_response["Items"]
|
||||
# print(emby_search_results)
|
||||
except (requests.exceptions.RequestException, KeyError) as e:
|
||||
logging.warning(f"Error searching for track in Emby: {track_name}")
|
||||
logging.warning(f"Error message: {str(e)}")
|
||||
|
||||
return emby_search_results
|
||||
|
||||
def get_sessions(self):
|
||||
url = self._build_url(f'Sessions')
|
||||
response = self._get_request(url)
|
||||
return response
|
||||
|
||||
def play_item(self, session_id, item_id):
|
||||
url = self._build_url(f'Sessions/{session_id}/Playing', {'ItemIds': item_id, 'PlayCommand': 'PlayNow'})
|
||||
response = self._post_request(url)
|
||||
return response
|
||||
|
||||
def send_message(self, session_id, message):
|
||||
url = self._build_url(f'Sessions/{session_id}/Message', {'Text': message})
|
||||
response = self._post_request(url)
|
||||
return response
|
||||
|
||||
def get_movies(self, limit=50, is_played=None, is_favorite=None):
|
||||
return self.get_media(limit, "Movie", is_played, is_favorite)
|
||||
|
||||
def get_media(self, limit=50, item_types="Movie", genre=None, is_played=None, is_favorite=None,
|
||||
external_id=None, name=None, year=None):
|
||||
|
||||
params = {'Recursive': 'true', 'IncludeItemTypes': item_types, "Limit": limit}
|
||||
|
||||
if is_played is not None:
|
||||
params['IsPlayed'] = str(is_played)
|
||||
if is_favorite is not None:
|
||||
params['IsFavorite'] = str(is_favorite)
|
||||
if genre is not None:
|
||||
params['Genres'] = genre
|
||||
if external_id is not None:
|
||||
params['AnyProviderIdEquals'] = external_id
|
||||
if name is not None:
|
||||
params['Name'] = name
|
||||
if year is not None:
|
||||
params['ProductionYear'] = str(year)
|
||||
|
||||
url = self._build_url(f'Users/{self.user_id}/Items', params)
|
||||
response = self._get_request(url)
|
||||
items = response.get('Items', [])
|
||||
random.shuffle(items)
|
||||
return items[:limit]
|
||||
|
||||
def get_liked_movies(self, limit=50):
|
||||
return self.get_movies(limit, is_favorite=True)
|
||||
|
||||
def get_unwatched_movies(self, limit=50):
|
||||
return self.get_movies(limit, is_played=False)
|
||||
|
||||
def get_watched_series(self, limit=50):
|
||||
return self.get_media(limit, "Series", is_played=True)
|
||||
|
||||
def get_movies_by_genre(self, limit=50, genre="Action"):
|
||||
return self.get_media(limit, "Movie", genre=genre)
|
||||
|
||||
# @staticmethod
|
||||
# def create_poster(path, text, root_path, icon_path=f'/resources/icons/tv.png'):
|
||||
# width, height = 400, 600
|
||||
# start, end = (233, 0, 4), (88, 76, 76)
|
||||
# angle = -160
|
||||
# font_path = f'{root_path}/resources/fonts/OpenSans-SemiBold.ttf' # path to your .ttf font file
|
||||
#
|
||||
# image_creator = PosterImageCreator(width, height, "cyan-teal", angle, font_path)
|
||||
# img = image_creator.create_gradient().add_icon_with_text(icon_path, text)
|
||||
#
|
||||
# img.save(path, quality=95)
|
||||
# return img
|
||||
|
||||
####
|
||||
# MediaList based Methods
|
||||
####
|
||||
|
||||
# def search_for_external_ids(self, media_item: MediaItem) -> Optional[dict]:
|
||||
# item = None
|
||||
#
|
||||
# def search_id(external_id: str) -> Optional[dict]:
|
||||
# try:
|
||||
# search_results = self.get_media(external_id=external_id)
|
||||
# if search_results and search_results[0]['Type'] != 'Trailer':
|
||||
# return search_results[0]
|
||||
# except Exception as e:
|
||||
# print(f"Failed searching for {external_id} due to {e}")
|
||||
# return None
|
||||
#
|
||||
# try:
|
||||
# imdb_result = search_id(f"imdb.{media_item.providers.imdbId}")
|
||||
# tvdb_result = search_id(f"tvdb.{media_item.providers.tvdbId}")
|
||||
# except Exception as e:
|
||||
# print(f"Failed searching for {media_item} due to {e}")
|
||||
# return None
|
||||
#
|
||||
# if imdb_result:
|
||||
# item = imdb_result
|
||||
# elif tvdb_result:
|
||||
# item = tvdb_result
|
||||
# return item
|
||||
#
|
||||
# def create_collection_from_list(self, media_list: MediaList):
|
||||
# collection = self.create_collection(media_list.name, media_list.type, media_list.sortName)
|
||||
# # search emby for the items
|
||||
# # add the first result to the collection
|
||||
#
|
||||
# print('-------------', media_list.items)
|
||||
# for item in media_list.items:
|
||||
# print('-------------', item)
|
||||
# media_item = self.search_for_external_ids(item)
|
||||
# if media_item:
|
||||
# self.add_item_to_collection(collection['Id'], media_item['Id'])
|
||||
# return collection
|
||||
|
||||
def delete_collection_items(self, collection_id):
|
||||
items = self.get_collection_items(collection_id)
|
||||
for item in items:
|
||||
self.delete_item_from_collection(collection_id, item['Id'])
|
||||
return
|
||||
|
||||
# def update_collection_from_list(self, media_list: MediaList):
|
||||
# collection = self.get_list(media_list.sourceListId)
|
||||
# if collection:
|
||||
# self.delete_collection_items(collection['Id'])
|
||||
# return self.create_collection_from_list(media_list)
|
||||
#
|
||||
# def create_playlist_from_list(self, media_list: MediaList):
|
||||
# playlist = self.create_playlist(media_list.name, media_list.type, media_list.sortName)
|
||||
# # search emby for the items
|
||||
# # add the first result to the collection
|
||||
#
|
||||
# for item in media_list.items:
|
||||
# print('-------------', item)
|
||||
# emby_media_item = self.search_for_external_ids(item)
|
||||
# if emby_media_item:
|
||||
# self.add_item_to_playlist(playlist['Id'], emby_media_item['Id'])
|
||||
# return playlist
|
||||
#
|
||||
# def update_playlist_from_list(self, media_list: MediaList):
|
||||
# playlist = self.get_list(media_list.sourceListId)
|
||||
# if playlist:
|
||||
# self.delete_collection_items(playlist['Id'])
|
||||
# return self.create_playlist_from_list(media_list)
|
||||
|
||||
def upload_image_from_url(self, sourceListId, poster, root_path):
|
||||
if poster is None:
|
||||
print('no poster provided')
|
||||
return None
|
||||
|
||||
# if the media_list.poster is a url, download the image and upload it to the provider
|
||||
|
||||
print('downloading image from url')
|
||||
response = requests.get(poster, stream=True)
|
||||
if response.status_code == 200:
|
||||
# Assuming _get_request is using the requests library.
|
||||
# Use BytesIO to convert the response content into a file-like object so it can be opened by PIL
|
||||
img = Image.open(BytesIO(response.content)).convert('RGBA')
|
||||
|
||||
poster_location = f'{root_path}/poster.png'
|
||||
img.save(poster_location, quality=95)
|
||||
self.upload_image(sourceListId, poster_location)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import logging
|
||||
|
||||
import spotipy
|
||||
from fuzzywuzzy import fuzz
|
||||
from spotipy import SpotifyOAuth
|
||||
from src.utils.string import StringUtils
|
||||
|
||||
|
||||
class SpotifyClient:
|
||||
def __init__(self, client_id, client_secret, redirect_uri, scope):
|
||||
# Authenticate with Spotify API
|
||||
self.sp = spotipy.Spotify(
|
||||
auth_manager=SpotifyOAuth(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=redirect_uri,
|
||||
scope=scope,
|
||||
)
|
||||
)
|
||||
|
||||
def match_artists(self, spotify_artists, emby_artists):
|
||||
"""
|
||||
Match Spotify artists with Emby artists using fuzzy string matching.
|
||||
Returns the maximum artist similarity score found.
|
||||
"""
|
||||
max_artist_score = 0
|
||||
for spotify_artist in spotify_artists:
|
||||
spotify_artist_name = StringUtils.clean_string(spotify_artist["name"])
|
||||
for emby_artist in emby_artists:
|
||||
emby_artist_name = StringUtils.clean_string(emby_artist)
|
||||
artist_score = fuzz.ratio(spotify_artist_name, emby_artist_name)
|
||||
max_artist_score = max(max_artist_score, artist_score)
|
||||
return max_artist_score
|
||||
|
||||
def match_song(self, spotify_song, emby_song):
|
||||
"""
|
||||
Match a Spotify song with an Emby song using fuzzy string matching.
|
||||
"""
|
||||
try:
|
||||
spotify_title = StringUtils.clean_string(spotify_song["name"])
|
||||
spotify_artists = spotify_song["artists"]
|
||||
|
||||
emby_title = StringUtils.clean_string(emby_song["Name"])
|
||||
emby_artists = emby_song["Artists"]
|
||||
|
||||
title_ratio = fuzz.ratio(spotify_title, emby_title)
|
||||
artist_ratio = self.match_artists(spotify_artists, emby_artists)
|
||||
|
||||
# Adjust the thresholds as needed
|
||||
title_threshold = 90
|
||||
artist_threshold = 80
|
||||
combined_threshold = 170 # Sum of title and artist thresholds
|
||||
|
||||
if title_ratio >= title_threshold and artist_ratio >= artist_threshold:
|
||||
combined_score = title_ratio + artist_ratio
|
||||
if combined_score >= combined_threshold:
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error matching song: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_playlist_tracks(self, playlist_id):
|
||||
results = self.sp.playlist_tracks(playlist_id)
|
||||
tracks = results["items"]
|
||||
while results["next"]:
|
||||
results = self.sp.next(results)
|
||||
tracks.extend(results["items"])
|
||||
return tracks
|
||||
|
||||
def get_playlists(self):
|
||||
return self.sp.current_user_playlists()
|
||||
|
||||
def get_spotify(self):
|
||||
return self.sp
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# config.py
|
||||
|
||||
class Config:
|
||||
# Spotify API credentials
|
||||
SPOTIFY_CLIENT_ID = "402b29e6ea6e4296b351d9d95c65b6d9"
|
||||
SPOTIFY_CLIENT_SECRET = "5ec3c861ca7141d9b01f75e1eca71bdc"
|
||||
SPOTIFY_REDIRECT_URI = "https://my.home-assistant.io/redirect/oauth"
|
||||
SPOTIFY_SCOPE = "playlist-read-private"
|
||||
|
||||
# Emby API credentials
|
||||
EMBY_URL: str = "http://192.168.0.120:8096"
|
||||
EMBY_API_KEY = "8dc2564d98064333bae3e6eae4a5f8b4"
|
||||
EMBY_USER_ID = "174eb36806924abdad60d6e18ca6da61"
|
||||
EMBY_USERNAME = 'Faiyt'
|
||||
EMBY_PASSWORD = 'dragon'
|
||||
EMBY_CLIENT = "SpotEmbySync"
|
||||
EMBY_DEVICE = "PC"
|
||||
EMBY_DEVICE_ID = "your_emby_device_id"
|
||||
EMBY_VERSION = "1.0.0"
|
||||
|
||||
|
||||
|
||||
# spotify_client_id = "402b29e6ea6e4296b351d9d95c65b6d9"
|
||||
# spotify_client_secret = "5ec3c861ca7141d9b01f75e1eca71bdc"
|
||||
# spotify_redirect_uri = "https://my.home-assistant.io/redirect/oauth"
|
||||
# spotify_scope = "playlist-read-private"
|
||||
#
|
||||
# # Emby API credentials
|
||||
# emby_url = "https://emby.faiyts.media"
|
||||
# emby_api_key = "8dc2564d98064333bae3e6eae4a5f8b4"
|
||||
# emby_user_id = "174eb36806924abdad60d6e18ca6da61"
|
||||
# emby_client = "SpotEmbySync"
|
||||
# emby_device = "PC"
|
||||
# emby_device_id = "your_emby_device_id"
|
||||
# emby_version = "1.0.0"
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# health.py
|
||||
from config import Config
|
||||
import spotipy
|
||||
from spotipy.oauth2 import SpotifyOAuth
|
||||
import requests
|
||||
|
||||
def test_spotify_connection():
|
||||
try:
|
||||
spotify = spotipy.Spotify(auth_manager=SpotifyOAuth(
|
||||
client_id=Config.SPOTIFY_CLIENT_ID,
|
||||
client_secret=Config.SPOTIFY_CLIENT_SECRET,
|
||||
redirect_uri=Config.SPOTIFY_REDIRECT_URI,
|
||||
scope=Config.SPOTIFY_SCOPE
|
||||
))
|
||||
user = spotify.current_user()
|
||||
print("Spotify connection successful.")
|
||||
print(f"Logged in as: {user['display_name']} ({user['id']})")
|
||||
except Exception as e:
|
||||
print(f"Spotify connection failed: {str(e)}")
|
||||
|
||||
def test_emby_connection():
|
||||
try:
|
||||
headers = {
|
||||
"X-Emby-Token": Config.EMBY_API_KEY,
|
||||
"X-Emby-Client": Config.EMBY_CLIENT,
|
||||
"X-Emby-Device-Name": Config.EMBY_DEVICE,
|
||||
"X-Emby-Device-Id": Config.EMBY_DEVICE_ID,
|
||||
"X-Emby-Client-Version": Config.EMBY_VERSION
|
||||
}
|
||||
response = requests.get(f"{Config.EMBY_URL}/emby/System/Info", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Emby connection successful.")
|
||||
data = response.json()
|
||||
print(f"Emby server version: {data['Version']}")
|
||||
else:
|
||||
print(f"Emby connection failed. Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"Emby connection failed: {str(e)}")
|
||||
|
||||
|
||||
def get_emby_profiles():
|
||||
try:
|
||||
headers = {
|
||||
"X-Emby-Token": Config.EMBY_API_KEY,
|
||||
"X-Emby-Client": Config.EMBY_CLIENT,
|
||||
"X-Emby-Device-Name": Config.EMBY_DEVICE,
|
||||
"X-Emby-Device-Id": Config.EMBY_DEVICE_ID,
|
||||
"X-Emby-Client-Version": Config.EMBY_VERSION
|
||||
}
|
||||
response = requests.get(f"{Config.EMBY_URL}/emby/Users", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
profiles_data = response.json()
|
||||
print("Valid profiles:")
|
||||
for profile in profiles_data:
|
||||
print(f"- ID: {profile['Id']}, Name: {profile['Name']}")
|
||||
else:
|
||||
print(f"Failed to retrieve profiles. Status code: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"Failed to retrieve profiles: {str(e)}")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing Spotify connection...")
|
||||
test_spotify_connection()
|
||||
print()
|
||||
|
||||
print("Testing Emby connection...")
|
||||
test_emby_connection()
|
||||
|
||||
print("Retrieving Emby profiles...")
|
||||
get_emby_profiles()
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from src.clients.emby_client import Emby
|
||||
from services.playlist_service import PlaylistService
|
||||
from src.config import Config
|
||||
|
||||
# Create an instance of the Emby client for the source user
|
||||
source_emby = Emby(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
||||
|
||||
# Create an array of target usernames
|
||||
target_usernames = ["Alyssa", "Dalton", "Laura", "Zuko", "Azula","Brittany","Adam"]
|
||||
|
||||
# Get all the playlists for the source user
|
||||
playlists = source_emby.get_playlists()
|
||||
|
||||
# Iterate over each playlist
|
||||
for playlist in playlists:
|
||||
# Iterate over the target usernames
|
||||
for target_username in target_usernames:
|
||||
# Check if the target username is in the playlist name
|
||||
if target_username in playlist["Name"]:
|
||||
try:
|
||||
# Create an instance of the Emby client for the target user
|
||||
target_emby = Emby(Config.EMBY_URL, target_username, '')
|
||||
|
||||
# Create an instance of the PlaylistService
|
||||
playlist_service = PlaylistService(source_emby, target_emby)
|
||||
|
||||
# Copy the playlist to the target user
|
||||
new_playlist = playlist_service.copy_playlist_by_usernames(Config.EMBY_USERNAME, target_username, playlist["Id"])
|
||||
print(f"Playlist '{playlist['Name']}' copied successfully to user '{target_username}'. New playlist ID: {new_playlist['Id']}")
|
||||
except ValueError as e:
|
||||
print(f"Error copying playlist '{playlist['Name']}' to user '{target_username}': {str(e)}")
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from src.clients.emby_client import Emby, EmbyLibraryItemType
|
||||
|
||||
# Array of TV shows
|
||||
shows = [
|
||||
{"id": "show1-id", "name": "First Show"},
|
||||
{"id": "show2-id", "name": "Second Show"},
|
||||
{"id": "show3-id", "name": "Third Show"}
|
||||
]
|
||||
|
||||
def mesh_shows(emby_client):
|
||||
all_episodes = []
|
||||
for show in shows:
|
||||
seasons = emby_client.get_seasons(show["id"])
|
||||
for season in seasons:
|
||||
episodes = emby_client.get_episodes(show["id"], season["Id"])
|
||||
all_episodes.append(episodes)
|
||||
|
||||
max_episodes = max(len(episodes) for episodes in all_episodes)
|
||||
|
||||
playlist = emby_client.create_playlist("Meshed Shows", EmbyLibraryItemType.VIDEO.value)
|
||||
playlist_id = playlist["Id"]
|
||||
|
||||
for i in range(max_episodes):
|
||||
for j, episodes in enumerate(all_episodes):
|
||||
if i < len(episodes):
|
||||
episode = episodes[i]
|
||||
emby_client.add_item_to_playlist(playlist_id, episode["Id"])
|
||||
print(f"Adding {shows[j]['name']} S{episode['ParentIndexNumber']:02}E{episode['IndexNumber']:02} to the playlist")
|
||||
|
||||
# Balance the shows based on the number of episodes
|
||||
remaining_episodes = len(episodes) - (i + 1)
|
||||
other_remaining_episodes = [len(eps) - (i + 1) for eps in all_episodes[:j] + all_episodes[j+1:]]
|
||||
if remaining_episodes < min(other_remaining_episodes):
|
||||
# Add additional episodes of the current show
|
||||
extra_episodes = min(other_remaining_episodes) - remaining_episodes
|
||||
for k in range(extra_episodes):
|
||||
if i + k + 1 < len(episodes):
|
||||
extra_episode = episodes[i + k + 1]
|
||||
emby_client.add_item_to_playlist(playlist_id, extra_episode["Id"])
|
||||
print(f"Adding {shows[j]['name']} S{extra_episode['ParentIndexNumber']:02}E{extra_episode['IndexNumber']:02} to the playlist")
|
||||
|
||||
print("Playlist created successfully!")
|
||||
|
||||
def main():
|
||||
emby_client = Emby(EMBY_URL, EMBY_USERNAME, EMBY_API_KEY)
|
||||
mesh_shows(emby_client)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,38 @@
|
|||
from src.clients.emby_client import EmbyClient
|
||||
|
||||
class PlaylistService:
|
||||
def __init__(self, source_emby: EmbyClient, target_emby: EmbyClient):
|
||||
self.emby = source_emby
|
||||
self.target_emby = target_emby
|
||||
|
||||
def copy_playlist(self, source_user_id: int , target_user_id: int, playlist_id: str):
|
||||
# Get the playlist details
|
||||
playlist = self.emby.get_list(playlist_id)
|
||||
|
||||
print(f"Copying playlist {playlist_id} to {target_user_id}")
|
||||
# print(playlist)
|
||||
|
||||
# Create a new playlist for the target user
|
||||
new_playlist = self.target_emby.create_playlist(playlist["Name"], playlist["Type"], user_id=target_user_id)
|
||||
|
||||
# Get the items from the source playlist
|
||||
playlist_items, _ = self.emby.get_list_items(playlist_id)
|
||||
|
||||
# Add the items to the new playlist
|
||||
for item in playlist_items:
|
||||
self.target_emby.add_item_to_playlist(new_playlist["Id"], item["Id"])
|
||||
|
||||
return new_playlist
|
||||
|
||||
def copy_playlist_by_usernames(self, source_username: str, target_username: str, playlist_id: str):
|
||||
|
||||
source_user = self.emby.get_user_by_username(source_username)
|
||||
target_user = self.target_emby.get_user_by_username(target_username)
|
||||
|
||||
if source_user is None:
|
||||
raise ValueError(f"Source user '{source_username}' not found.")
|
||||
if target_user is None:
|
||||
raise ValueError(f"Target user '{target_username}' not found.")
|
||||
|
||||
# Call the copy_playlist method with the user IDs
|
||||
return self.copy_playlist(source_user['Id'], target_user["Id"], playlist_id)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,172 @@
|
|||
import unittest
|
||||
from src.clients.emby_client import Emby
|
||||
from src.config import Config
|
||||
|
||||
|
||||
class TestEmbyIntegration(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server_url = Config.EMBY_URL
|
||||
self.username = Config.EMBY_USERNAME
|
||||
self.api_key = Config.EMBY_API_KEY
|
||||
self.password = Config.EMBY_PASSWORD
|
||||
self.emby = Emby(self.server_url, self.username, self.password)
|
||||
|
||||
def test_get_collections(self):
|
||||
collections = self.emby.get_collections()
|
||||
self.assertIsInstance(collections, list)
|
||||
# Assert that the collections have the expected properties
|
||||
for collection in collections:
|
||||
self.assertIn("Name", collection)
|
||||
self.assertIn("Id", collection)
|
||||
|
||||
def test_add_and_remove_items_from_collection(self):
|
||||
# Get the ID of the first item in the movies library
|
||||
movies_library = next((library for library in self.emby.get_libraries() if library["Name"] == "Movies"), None)
|
||||
if movies_library is None:
|
||||
self.skipTest("Movies library not found")
|
||||
|
||||
movies, _ = self.emby.get_items_from_library(movies_library["Name"])
|
||||
if not movies:
|
||||
self.skipTest("No movies found in the library")
|
||||
|
||||
item_id = movies[0]["Id"]
|
||||
|
||||
# Create a new collection
|
||||
collection_name = "Test Collection"
|
||||
collection_type = "movies"
|
||||
collection = self.emby.create_collection(collection_name, collection_type)
|
||||
self.assertEqual(collection["Name"], collection_name)
|
||||
self.assertIsNotNone(collection["Id"])
|
||||
|
||||
# Add the item to the collection
|
||||
self.emby.add_item_to_collection(collection["Id"], item_id)
|
||||
|
||||
# Verify that the item is added to the collection
|
||||
collection_items, _ = self.emby.get_collection_items(collection["Id"])
|
||||
self.assertIn(item_id, [item["Id"] for item in collection_items])
|
||||
|
||||
# Delete the item from the collection
|
||||
self.emby.delete_item_from_collection(collection["Id"], item_id)
|
||||
|
||||
# Verify that the item is deleted from the collection
|
||||
collection_items, _ = self.emby.get_collection_items(collection["Id"])
|
||||
self.assertNotIn(item_id, [item["Id"] for item in collection_items])
|
||||
|
||||
def test_create_collection(self):
|
||||
# Create a new collection
|
||||
collection_name = "Test Collection"
|
||||
collection_type = "Movie"
|
||||
collection = self.emby.create_collection(collection_name, collection_type)
|
||||
self.assertEqual(collection["Name"], collection_name)
|
||||
self.assertIsNotNone(collection["Id"])
|
||||
|
||||
# Delete the collection
|
||||
self.emby.delete_collection(collection["Id"])
|
||||
|
||||
# Verify that the collection is deleted
|
||||
collections = self.emby.get_collections()
|
||||
self.assertNotIn(collection["Id"], [c["Id"] for c in collections])
|
||||
|
||||
# Add more integration tests for other methods
|
||||
|
||||
def test_create_and_delete_playlist(self):
|
||||
# Get the ID of the first item in the movies library
|
||||
movies_library = next((library for library in self.emby.get_libraries() if library["Name"] == "Movies"), None)
|
||||
if movies_library is None:
|
||||
self.skipTest("Movies library not found")
|
||||
|
||||
movies, _ = self.emby.get_items_from_library(movies_library["Name"])
|
||||
if not movies:
|
||||
self.skipTest("No movies found in the library")
|
||||
|
||||
item_id = movies[0]["Id"]
|
||||
|
||||
# Create a new playlist
|
||||
playlist_name = "Test Playlist"
|
||||
playlist_type = "movies"
|
||||
playlist = self.emby.create_playlist(playlist_name, playlist_type)
|
||||
self.assertEqual(playlist["Name"], playlist_name)
|
||||
self.assertIsNotNone(playlist["Id"])
|
||||
|
||||
# Add the item to the playlist
|
||||
self.emby.add_item_to_playlist(playlist["Id"], item_id)
|
||||
|
||||
# Verify that the item is added to the playlist
|
||||
playlist_items, _ = self.emby.get_list_items(playlist["Id"])
|
||||
self.assertIn(item_id, [item["Id"] for item in playlist_items])
|
||||
|
||||
# Delete the item from the playlist
|
||||
self.emby.delete_item_from_playlist(playlist["Id"], item_id)
|
||||
|
||||
# Verify that the item is deleted from the playlist
|
||||
playlist_items, _ = self.emby.get_list_items(playlist["Id"])
|
||||
self.assertNotIn([item_id], [item["Id"] for item in playlist_items])
|
||||
|
||||
# Delete the playlist
|
||||
self.emby.delete_playlist(playlist["Id"])
|
||||
|
||||
# Verify that the playlist is deleted
|
||||
playlists = self.emby.get_playlists()
|
||||
self.assertNotIn(playlist["Id"], [p["Id"] for p in playlists])
|
||||
|
||||
def test_search_for_track_existing(self):
|
||||
# Test searching for a track that exists in the Emby library
|
||||
track_name = "Away from the Sun"
|
||||
artist_name = "3 Doors Down"
|
||||
search_results = self.emby.search_for_track(track_name, artist_name)
|
||||
print(search_results)
|
||||
self.assertIsNotNone(search_results)
|
||||
self.assertGreater(len(search_results), 0)
|
||||
print(len(search_results))
|
||||
has_match = False
|
||||
for track in search_results:
|
||||
if track["Name"].lower() == track_name.lower():
|
||||
for artist in track["Artists"]:
|
||||
if artist.lower() == artist_name.lower():
|
||||
has_match = True
|
||||
self.assertTrue(has_match)
|
||||
|
||||
def test_search_for_track_nonexistent(self):
|
||||
# Test searching for a track that doesn't exist in the Emby library
|
||||
track_name = "Nonexistent Track"
|
||||
artist_name = "Nonexistent Artist"
|
||||
search_results = self.emby.search_for_track(track_name, artist_name)
|
||||
self.assertIsNotNone(search_results)
|
||||
self.assertEqual(len(search_results), 0)
|
||||
|
||||
# def test_search_for_track_partial_match(self):
|
||||
# # Test searching for a track with partial track name and artist name
|
||||
# track_name = "away from"
|
||||
# artist_name = "3 doors"
|
||||
# search_results = self.emby.search_for_track(track_name, artist_name)
|
||||
# self.assertIsNotNone(search_results)
|
||||
# self.assertGreater(len(search_results), 0)
|
||||
# for track in search_results:
|
||||
# self.assertIn(track_name.lower(), track["Name"].lower())
|
||||
# self.assertIn(artist_name.lower(), [artist["Name"].lower() for artist in track["Artists"]])
|
||||
|
||||
def test_search_for_track_empty_query(self):
|
||||
# Test searching for a track with empty track name and artist name
|
||||
track_name = ""
|
||||
artist_name = ""
|
||||
search_results = self.emby.search_for_track(track_name, artist_name)
|
||||
self.assertIsNotNone(search_results)
|
||||
# Depending on your Emby server, empty search query may return all tracks or no tracks
|
||||
# Adjust the assertion based on your expected behavior
|
||||
# self.assertGreater(len(search_results), 0)
|
||||
# self.assertEqual(len(search_results), 0)
|
||||
|
||||
def test_search_for_track_special_characters(self):
|
||||
# Test searching for a track with special characters in track name and artist name
|
||||
track_name = "Track with !@#$%^&*()"
|
||||
artist_name = "Artist with !@#$%^&*()"
|
||||
search_results = self.emby.search_for_track(track_name, artist_name)
|
||||
self.assertIsNotNone(search_results)
|
||||
# Depending on your Emby server, special characters may affect the search results
|
||||
# Adjust the assertions based on your expected behavior
|
||||
# self.assertGreater(len(search_results), 0)
|
||||
# self.assertEqual(len(search_results), 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.clients.emby_client import Emby
|
||||
from src.config import Config
|
||||
|
||||
class TestEmby(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server_url = Config.EMBY_URL
|
||||
self.username = Config.EMBY_USERNAME
|
||||
self.api_key = Config.EMBY_API_KEY
|
||||
self.emby = Emby(self.server_url, self.username, self.api_key)
|
||||
self.emby.user = {"Id": Config.EMBY_USER_ID}
|
||||
self.emby.user_id = Config.EMBY_USER_ID
|
||||
|
||||
@patch('src.clients.emby.Emby._get_request')
|
||||
def test_get_collections(self, mock_get_request):
|
||||
mock_response = {"Items": [{"Name": "Collection1"}, {"Name": "Collection2"}]}
|
||||
mock_get_request.return_value = mock_response
|
||||
collections = self.emby.get_collections()
|
||||
self.assertEqual(len(collections), 2)
|
||||
self.assertEqual(collections[0]["Name"], "Collection1")
|
||||
self.assertEqual(collections[1]["Name"], "Collection2")
|
||||
|
||||
@patch('src.clients.emby.Emby._get_request')
|
||||
def test_get_collection_by_name(self, mock_get_request):
|
||||
mock_response = {"Items": [{"Name": "Collection1", "Type": "boxset"}, {"Name": "Collection2", "Type": "boxset"}]}
|
||||
mock_get_request.return_value = mock_response
|
||||
collection = self.emby.get_collection_by_name("Collection1")
|
||||
self.assertIsNotNone(collection)
|
||||
self.assertEqual(collection["Name"], "Collection1")
|
||||
|
||||
@patch('src.clients.emby.Emby._post_request')
|
||||
def test_create_collection(self, mock_post_request):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"Name": "NewCollection", "Id": "newcollectionid"}
|
||||
mock_post_request.return_value = mock_response
|
||||
|
||||
with patch('src.clients.emby.Emby.get_items_by_type') as mock_get_items_by_type:
|
||||
mock_get_items_by_type.return_value = [{"Id": "itemid"}]
|
||||
with patch('src.clients.emby.Emby.delete_item_from_collection') as mock_delete_item_from_collection:
|
||||
collection = self.emby.create_collection("NewCollection", "boxset")
|
||||
self.assertEqual(collection["Name"], "NewCollection")
|
||||
self.assertEqual(collection["Id"], "newcollectionid")
|
||||
mock_delete_item_from_collection.assert_called_once_with("newcollectionid", "itemid")
|
||||
|
||||
@patch('src.clients.emby.Emby._post_request')
|
||||
def test_delete_collection(self, mock_post_request):
|
||||
self.emby.delete_collection("collectionid")
|
||||
mock_post_request.assert_called_once_with(f"{self.server_url}/emby/Items/collectionid/Delete?api_key={self.api_key}&X-Emby-Token={self.api_key}")
|
||||
|
||||
# Add more test methods for other functions in the Emby class
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,16 @@
|
|||
import re
|
||||
|
||||
|
||||
class StringUtils:
|
||||
def clean_string(s):
|
||||
"""
|
||||
Helper function to remove non-alphanumeric characters and convert to lowercase.
|
||||
"""
|
||||
return re.sub(r"[^a-zA-Z0-9\s]", "", s).lower()
|
||||
|
||||
def remove_special_characters(text):
|
||||
# Remove special characters, brackets, parentheses, and their contents
|
||||
cleaned_text = re.sub(r"[\[\]\(\)]*", "", text)
|
||||
cleaned_text = re.sub(r"[^a-zA-Z0-9\s]", "", cleaned_text)
|
||||
return cleaned_text
|
||||
|
||||
Loading…
Reference in New Issue