feature: added dockerfile and docker-entrypoint.sh

This commit is contained in:
unfaiyted 2024-08-02 14:25:11 -05:00
parent 62fc4201ca
commit c0cb7ecbee
23 changed files with 7974 additions and 201 deletions

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install system dependencies and cron
RUN apt-get update && \
apt-get install -y --no-install-recommends cron #&& \
# rm -rf /var/lib/apt/lists/*
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir --upgrade pip
RUN pip install --no-cache-dir --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt
# Install cron
#RUN apt-get update && apt-get -y install cron
# Copy crontab file to the cron.d directory
COPY app-cron /etc/cron.d/app-cron
# Give execution rights on the cron job
RUN chmod 0644 /etc/cron.d/app-cron
# Apply cron job
RUN crontab /etc/cron.d/app-cron
# Create the log file to be able to run tail
RUN touch /var/log/cron.log
# Run the command on container startup
CMD cron && tail -f /var/log/cron.log
# Copy the entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set the entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]

1
app-cron Normal file
View File

@ -0,0 +1 @@
0 2 * * * /usr/local/bin/python /app/src/main.py >> /var/log/cron.log 2>&1

10
docker-entrypoint.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/sh
# Replace the cron schedule with the one from .env
#sed -i "s|* * * * *|$CRON_SCHEDULE|g" /etc/cron.d/app-cron
# Apply the updated cron job
#crontab /etc/cron.d/app-cron
# Start cron in the foreground
cron -f

BIN
requirements.txt Normal file

Binary file not shown.

View File

@ -1 +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"}
{"access_token": "BQCOqPiTZyLWYouW3TxjCpFO3m15BcmPJXXYXhLZ-k36BaTCh7IVkoFiHh2uXXydEIMabbHuk9mZsImyw-lfZ1DS-_NNCEdR1z1Hy0xzBlM_OJ3ItOHMIW-xodY3czcUJWcQUvcLduUqpGiWEod_lbxN9JaYx35JA9zFozGfwba82UjrMtDsRqYQg0PMhqhqb-VCSBClg8ue0RGR8D3IdmketZRLq7Se46YvWAOBG_td-YI", "token_type": "Bearer", "expires_in": 3600, "scope": "playlist-read-private", "expires_at": 1722629879, "refresh_token": "AQAA4rKcyTFqaVctXWaxmE3d0LqLJu3ms91BNXrlJeYfy_1fYmCyQ3VyX6rO6K0O4iGyRVNWT8lcJB6MGRuC5rLXG85ZFB2XQtpcab6ECyKrIQNcMNU7AJj_eiZgfTwNtFk"}

12
src/README.md Normal file
View File

@ -0,0 +1,12 @@
# Emby Scripts
You can run any number of actions from the action folder directly.
Actions are in `src/actions`
There is a venv that has the variables for everything.
## Actions
- find_unmatched_songs.
-

View File

@ -1 +1 @@
{"access_token": "BQAYOPW8HTsv1vW7-DIAmNRqqXqG7XqddSEB9217h1jxSBqIu5yjvh13FB8sE-aW5k0wtycRdTqiUPcj4fLGPXvE3iNgbO059EqDmOzPWrAdzXBIgwal3SUouI2j2z_8tYTcqFh0VzqgcVSnX5AsEyhQcdJA2QusfItEPTI1jkv69RuAXURYtloPoCPjhkfCoum4FFrvya5xDSN06sgB2ZPA9euT0_9_f8oQbXGnZFsBPA", "token_type": "Bearer", "expires_in": 3600, "scope": "playlist-read-private", "expires_at": 1715107319, "refresh_token": "AQBij3uDeV9G6Z71rKFATWpcHJLhggDPYabbTEuvebdSKEqRY_5VqS98wkx3u037d7nBlaTHIQpeSqkbbOmLdIut56TZ3Yiva56R4t6PV22LSY0wUdTYfwSjP_nWr_jP1wU"}
{"access_token": "BQDLUIwaf9wl9YwDlaD-kcAg1imJM99GzbVxIRkdasZ-i1cc0UlxCjkO9-m_dRMPVLjHedkX2mcffSj2XTJDFnrv92DrImZZmOD4AZJMo0FQsXWFyQHSYWdKqgxJg0xIx9a0Xz6-_sFQoPidjYxCS6o34WBOv7agj5V0ARJif90AUxvE9j5O813JRZjLFioCgmicjgcCXylMEm80OMXYCwfEWM3ZUgNVmZ3uqRLlUcHIw5c", "token_type": "Bearer", "expires_in": 3600, "scope": "playlist-read-private", "expires_at": 1722622536, "refresh_token": "AQBij3uDeV9G6Z71rKFATWpcHJLhggDPYabbTEuvebdSKEqRY_5VqS98wkx3u037d7nBlaTHIQpeSqkbbOmLdIut56TZ3Yiva56R4t6PV22LSY0wUdTYfwSjP_nWr_jP1wU"}

Binary file not shown.

View File

@ -0,0 +1,33 @@
from src.clients.emby_client import EmbyClient
from src.config import Config
from src.services.playlist_service import PlaylistService
#
# This is a simple example of copying a playlist to a target user
#
def copy_playlists_to_users(source_emby):
# Create an array of target usernames
target_usernames = ["Alyssa", "Dalton", "Laura", "Zuko", "Azula","Brittany","Adam","Chris","Jason"]
# 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 = EmbyClient(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)}")

View File

@ -0,0 +1,45 @@
# codebase/emby-scripts/src/actions/delete_duplicate_playlists.py
import logging
from src.config import Config
from src.clients.emby_client import EmbyClient
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
def delete_duplicate_playlists(emby):
# Initialize EmbyClient
# Get all playlists from Emby
all_playlists = emby.get_playlists()
# Dictionary to store playlists by name
playlists_by_name = {}
for playlist in all_playlists:
playlist_name = playlist['Name']
playlist_id = playlist['Id']
playlist_item_count = playlist.get('ChildCount', 0)
if playlist_name in playlists_by_name:
existing_playlist = playlists_by_name[playlist_name]
existing_item_count = existing_playlist.get('ChildCount', 0)
if playlist_item_count > existing_item_count:
# Delete the existing playlist with fewer items
emby.delete_playlist(existing_playlist['Id'])
logging.info(f"Deleted duplicate playlist: {existing_playlist['Name']} (ID: {existing_playlist['Id']}) with {existing_item_count} items")
playlists_by_name[playlist_name] = playlist
else:
# Delete the current playlist with fewer items
emby.delete_playlist(playlist_id)
logging.info(f"Deleted duplicate playlist: {playlist_name} (ID: {playlist_id}) with {playlist_item_count} items")
else:
playlists_by_name[playlist_name] = playlist
logging.info("Finished deleting duplicate playlists")
if __name__ == "__main__":
delete_duplicate_playlists()

View File

@ -2,11 +2,22 @@ import shlex
import sqlite3
import logging
import subprocess
import unicodedata
import re
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("unmatched_songs.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout)
]
)
# Set the path to the spotdl.exe file
@ -15,68 +26,96 @@ spotdl_path = "C:\\Users\\Dane Miller\\Downloads\\spotdl-4.2.5-win32.exe"
# Set the output directory where the songs will be saved
output_dir = "C:\\Users\\Dane Miller\\Music\\spotdl_downloads"
# Connect to the SQLite database
conn = sqlite3.connect('unmatched_songs.db')
c = conn.cursor()
def remove_special_characters(string, skip_non_ascii=False):
if skip_non_ascii and not all(ord(char) < 128 for char in string):
return None # Skip strings with non-ASCII characters
# Normalize Unicode characters
normalized = unicodedata.normalize('NFKD', string)
# Remove diacritics (accent marks)
without_diacritics = ''.join([c for c in normalized if not unicodedata.combining(c)])
# Replace spaces and other special characters with underscores
cleaned_string = re.sub(r'[^\w\s-]', '_', without_diacritics)
# Replace multiple consecutive underscores with a single underscore
cleaned_string = re.sub(r'_+', '_', cleaned_string)
# Remove leading and trailing underscores
cleaned_string = cleaned_string.strip('_')
# Remove non-ASCII characters
cleaned_string = cleaned_string.encode('ascii', 'ignore').decode('ascii')
if cleaned_string == '':
return None
def remove_special_characters(string):
# Remove single quotes, double quotes, slashes, and other special characters
cleaned_string = re.sub(r'[\'"\\/]', '', string)
return cleaned_string
# Create the table to store downloaded songs if it doesn't exist
c.execute('''CREATE TABLE IF NOT EXISTS downloaded_songs
(track_name TEXT, artist_name TEXT, album_name TEXT)''')
def find_unmatched_songs():
# Connect to the SQLite database
conn = sqlite3.connect('unmatched_songs.db')
c = conn.cursor()
# Create the table to store downloaded songs if it doesn't exist
c.execute('''CREATE TABLE IF NOT EXISTS downloaded_songs
(track_name TEXT, artist_name TEXT, album_name TEXT)''')
# Remove duplicates from the unmatched songs table
c.execute('''DELETE FROM unmatched_songs
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM unmatched_songs
GROUP BY track_name, artist_name, album_name
)''')
conn.commit()
# Remove duplicates from the unmatched songs table
c.execute('''DELETE FROM unmatched_songs
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM unmatched_songs
GROUP BY track_name, artist_name, album_name
)''')
conn.commit()
# Retrieve the unmatched songs from the database
c.execute('SELECT DISTINCT track_name, artist_name, album_name FROM unmatched_songs')
unmatched_songs = c.fetchall()
# Retrieve the unmatched songs from the database
c.execute('SELECT DISTINCT track_name, artist_name, album_name FROM unmatched_songs')
unmatched_songs = c.fetchall()
# Get the total number of unmatched songs
total_unmatched_songs = len(unmatched_songs)
# Get the total number of unmatched songs
total_unmatched_songs = len(unmatched_songs)
# Process each unmatched song
for index, song in enumerate(unmatched_songs, start=1):
track_name, artist_name, album_name = song
# Process each unmatched song
for index, song in enumerate(unmatched_songs, start=1):
track_name, artist_name, album_name = song
# Check if the song has already been downloaded
c.execute('SELECT COUNT(*) FROM downloaded_songs WHERE track_name = ? AND artist_name = ? AND album_name = ?',
(track_name, artist_name, album_name))
count = c.fetchone()[0]
cleaned_track = remove_special_characters(track_name)
cleaned_artist = remove_special_characters(artist_name)
if count > 0:
logging.info(f"Skipping '{track_name}' by {artist_name} - already downloaded")
continue
# Check if the song has already been downloaded
c.execute('SELECT COUNT(*) FROM downloaded_songs WHERE track_name = ? AND artist_name = ? AND album_name = ?',
(track_name, artist_name, album_name))
count = c.fetchone()[0]
logging.info(f"Processing unmatched song: '{track_name}' by {artist_name}")
if count > 0:
logging.info(f"Skipping '{track_name}' by {artist_name} - already downloaded")
continue
# Use spotdl to find and download the song
try:
command = f'"{spotdl_path}" download "{remove_special_characters(artist_name)} - {remove_special_characters(track_name)}" --output "{output_dir}"'
logging.info(f"Running command: {command}")
subprocess.run(command, shell=True, check=True)
logging.info(f"Successfully downloaded '{track_name}' by {artist_name}")
logging.info(f"Processing unmatched song: '{track_name}' by {artist_name}")
# Insert the downloaded song into the database
c.execute('INSERT INTO downloaded_songs VALUES (?, ?, ?)', (track_name, artist_name, album_name))
conn.commit()
except subprocess.CalledProcessError as e:
logging.error(f"Error downloading '{track_name}' by {artist_name}")
logging.error(f"Error message: {str(e)}")
# Use spotdl to find and download the song
try:
command = f'"{spotdl_path}" download "{cleaned_artist} - {cleaned_track}" --output "{output_dir}"'
logging.info(f"Running command: {command}")
subprocess.run(command, shell=True, check=True)
logging.info(f"Successfully downloaded '{track_name}' by {artist_name}")
# Calculate the percentage of songs processed
percentage = (index / total_unmatched_songs) * 100
logging.info(f"Progress: {index}/{total_unmatched_songs} ({percentage:.2f}%)")
# Insert the downloaded song into the database
c.execute('INSERT INTO downloaded_songs VALUES (?, ?, ?)', (track_name, artist_name, album_name))
conn.commit()
except subprocess.CalledProcessError as e:
logging.error(f"Error downloading '{track_name}' by {artist_name}")
logging.error(f"Error message: {str(e)}")
# Close the database connection
conn.close()
# Calculate the percentage of songs processed
percentage = (index / total_unmatched_songs) * 100
logging.info(f"Progress: {index}/{total_unmatched_songs} ({percentage:.2f}%)")
# Close the database connection
conn.close()
if __name__ == '__main__':
find_unmatched_songs()

View File

View File

@ -2,12 +2,13 @@ import spotipy
from spotipy.oauth2 import SpotifyOAuth
import requests
import logging
import json
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
from src.clients.spotify_client import SpotifyClient, clean_title
import sqlite3
# Configure logging
@ -16,137 +17,145 @@ logging.basicConfig(
)
# 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)
# 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)
# Connect to the SQLite database (it will be created if it doesn't exist)
conn = sqlite3.connect('unmatched_songs.db')
c = conn.cursor()
# Create the table to store unmatched songs if it doesn't exist
c.execute('''CREATE TABLE IF NOT EXISTS unmatched_songs
(playlist_name TEXT, track_name TEXT, artist_name TEXT, album_name TEXT)''')
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"]})"
)
# Get the playlist image from Spotify
playlist_image_data = spot.get_playlist_image(playlist_id)
if playlist_image_data:
try:
emby.upload_image_data(emby_playlist['Id'], playlist_image_data)
logging.info(f"Uploaded playlist cover image for '{playlist_name}'")
except requests.exceptions.RequestException as e:
logging.warning(f"Error uploading playlist cover image: {str(e)}")
else:
logging.warning(f"No playlist cover image found for '{playlist_name}'")
def try_match_and_add(track_name, artist_name, emby_playlist, emby, spot):
emby_search_results = emby.search_for_track(track_name, artist_name)
if emby_search_results:
for result in emby_search_results:
emby_item_id = result["Id"]
logging.debug(f'Matching {track_name} with {result["Name"]}')
if spot.match_song({"name": track_name, "artists": [{"name": artist_name}]}, result):
try:
emby.add_item_to_playlist(emby_playlist['Id'], emby_item_id)
# logging.info(f"Added '{track_name}' by {artist_name} to Emby playlist")
return True
except requests.exceptions.RequestException as e:
logging.warning(f"Error adding track to Emby playlist: {track_name}")
logging.warning(f"Error message: {str(e)}")
return False
def sync_spotify_playlists(spot, emby):
except (requests.exceptions.RequestException, KeyError) as e:
logging.error(f"Error creating Emby playlist: {playlist_name}")
logging.error(f"Error message: {str(e)}")
conn = sqlite3.connect('unmatched_songs.db')
c = conn.cursor()
# Create the table to store unmatched songs if it doesn't exist
c.execute('''CREATE TABLE IF NOT EXISTS unmatched_songs
(playlist_name TEXT, track_name TEXT, artist_name TEXT, album_name TEXT)''')
# Get the tracks in the Spotify playlist
tracks = spot.get_playlist_tracks(playlist_id)
playlists = spot.get_playlists()
featured_playlists = spot.get_featured_playlists()
# print(json.dumps(featured_playlists, indent=4))
logging.info(f"Processing {len(tracks)} tracks in Spotify playlist")
# Iterate over each track in the Spotify playlist
added_tracks = 0
unmatched_tracks = []
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)
all_playlists = playlists["items"] + featured_playlists["items"]
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
)
# Iterate over each Spotify playlist
for playlist in all_playlists:
playlist_name = playlist["name"]
playlist_id = playlist["id"]
playlist_owner = playlist["owner"]["display_name"]
logging.info(f"Processing Spotify playlist: {playlist_name} ({playlist_owner})")
if emby_search_results:
found_match = False
for result in emby_search_results:
if found_match:
break
emby_playlist_name = f"{playlist_name} ({playlist_owner})"
emby_item_id = result["Id"]
emby_playlist_search_results = emby.search(emby_playlist_name, 'Playlist')
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"
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})"
)
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"
# 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"]})"
)
# Get the playlist image from Spotify
playlist_image_data = spot.get_playlist_image(playlist_id)
if playlist_image_data:
try:
emby.upload_image_data(emby_playlist['Id'], playlist_image_data)
logging.info(f"Uploaded playlist cover image for '{playlist_name}'")
except requests.exceptions.RequestException as e:
logging.warning(f"Error uploading playlist cover image: {str(e)}")
else:
logging.warning(f"No playlist cover image found for '{playlist_name}'")
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
unmatched_tracks = []
for track in tracks:
track_name = track["track"]["name"]
artist_name = track["track"]["artists"][0]["name"]
album_name = track["track"]["album"]["name"]
if try_match_and_add(track_name, artist_name, emby_playlist, emby, spot):
added_tracks += 1
continue
clean_track_name = clean_title(track_name)
clean_artist_name = StringUtils.remove_special_characters(artist_name)
if clean_track_name != track_name or clean_artist_name != artist_name:
if try_match_and_add(clean_track_name, clean_artist_name, emby_playlist, emby, spot):
added_tracks += 1
continue
logging.warning(f"No match found for '{track_name}' by {artist_name} in Emby")
logging.warning(f"Cleaned track name: {clean_track_name}")
logging.warning(f"Cleaned artist name: {clean_artist_name}")
unmatched_tracks.append((playlist_name, track_name, artist_name, album_name))
# Insert the unmatched songs into the database
c.executemany('INSERT INTO unmatched_songs VALUES (?, ?, ?, ?)', unmatched_tracks)
conn.commit()
# Insert the unmatched songs into the database
c.executemany('INSERT INTO unmatched_songs VALUES (?, ?, ?, ?)', unmatched_tracks)
conn.commit()
# Calculate the match percentage for the current playlist
if len(tracks) > 0:
match_percentage = (added_tracks / len(tracks)) * 100
logging.info(f"Match percentage for playlist '{playlist_name}': {match_percentage:.2f}%")
# Calculate the match percentage for the current playlist
if len(tracks) > 0:
match_percentage = (added_tracks / len(tracks)) * 100
logging.info(f"Match percentage for playlist '{playlist_name}': {match_percentage:.2f}%")
added_tracks = 0
added_tracks = 0
# Close the database connection
conn.close()
# Close the database connection
conn.close()
if __name__ == "__main__":
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)
sync_spotify_playlists(spot, emby)

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
import logging
import re
import requests
import spotipy
from fuzzywuzzy import fuzz
from spotipy import SpotifyOAuth
from src.utils.string import StringUtils
@ -24,9 +25,13 @@ class SpotifyClient:
Match Spotify artists with Emby artists using fuzzy string matching.
Returns the maximum artist similarity score found.
"""
# print("MATCH ARTISTS")
# print(spotify_artists)
# print(emby_artists)
max_artist_score = 0
for spotify_artist in spotify_artists:
spotify_artist_name = StringUtils.clean_string(spotify_artist["name"])
spotify_artist_name = StringUtils.clean_string(spotify_artist)
for emby_artist in emby_artists:
emby_artist_name = StringUtils.clean_string(emby_artist)
artist_score = fuzz.ratio(spotify_artist_name, emby_artist_name)
@ -37,27 +42,49 @@ class SpotifyClient:
"""
Match a Spotify song with an Emby song using fuzzy string matching.
"""
# print(spotify_song)
# print(emby_song)
try:
spotify_title = StringUtils.clean_string(spotify_song["name"])
spotify_artists = spotify_song["artists"]
spotify_title = StringUtils.clean_string(spotify_song.get("name", "").lower())
spotify_artists = [artist.get("name", "").lower() for artist in spotify_song["artists"]]
emby_title = StringUtils.clean_string(emby_song["Name"])
emby_artists = emby_song["Artists"]
emby_title = StringUtils.clean_string(emby_song.get("Name", "").lower())
emby_artists = [artist.lower() for artist in emby_song.get("Artists", [])]
spotify_title_no_parentheses = remove_parentheses(spotify_title)
emby_title_no_parentheses = remove_parentheses(emby_title)
spotify_title_cleaned = clean_title(spotify_title)
emby_title_cleaned = clean_title(emby_title)
# print("SPOTIFY TITLE NO PARENTHESES", spotify_title_no_parentheses)
# print("EMBY TITLE NO PARENTHESES", emby_title_no_parentheses)
title_ratio = max(
fuzz.ratio(spotify_title, emby_title),
fuzz.ratio(spotify_title_no_parentheses, emby_title_no_parentheses),
fuzz.ratio(spotify_title_cleaned, emby_title_cleaned)
)
# print("TITLE RATIO", title_ratio)
title_ratio = fuzz.ratio(spotify_title, emby_title)
artist_ratio = self.match_artists(spotify_artists, emby_artists)
# print("ARTIST RATIO", artist_ratio)
# Adjust the thresholds as needed
title_threshold = 90
artist_threshold = 80
combined_threshold = 170 # Sum of title and artist thresholds
title_threshold = 85
artist_threshold = 75
combined_threshold = 165 # 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:
print(f"MATCH: {spotify_title} by {spotify_artists} vs {emby_title} by {emby_artists}")
return True
return False
except Exception as e:
logging.error(f"Error matching song: {str(e)}")
return False
@ -85,6 +112,7 @@ class SpotifyClient:
return image_data
return None
def get_liked_songs(self):
liked_songs = []
results = self.sp.current_user_saved_tracks()
@ -92,3 +120,75 @@ class SpotifyClient:
liked_songs.extend(results["items"])
results = self.sp.next(results)
return liked_songs
def get_featured_playlists(self):
return self.sp.featured_playlists()["playlists"]
pass
def remove_parentheses(text):
if text:
return re.sub(r'\([^)]*\)', '', text).strip()
else:
return text
def clean_title(title):
# Convert to lowercase
title = title.lower()
logging.info(f"Current title: {title}")
# Remove text after featuring, feat, ft, etc.
patterns = [
r'\(feat\..*?\)',
r'\(ft\..*?\)',
r'\(featuring.*?\)',
r'feat\..*',
r'ft\..*',
r'featuring.*',
r'\(with.*?\)',
r'\(prod\..*?\)',
r'\(produced by.*?\)',
r'- radio edit',
r'- single version',
r'- album version',
r'\(remaster(ed)?\)',
r'- remaster(ed)?',
r'\(remix\)',
r'- remix',
r'\(live\)',
r'- live',
r'\(acoustic\)',
r'- acoustic',
r'\(deluxe\)',
r'- deluxe',
r'\(extended\)',
r'- extended',
r'\(original mix\)',
r'- original mix',
r'\(album version\)',
r'- album version',
r'\(explicit\)',
r'- explicit',
r'\(clean\)',
r'- clean',
r'\d{4} (remaster|version)',
r'\d{4} digital (remaster|version)',
]
for pattern in patterns:
title = re.sub(pattern, '', title, flags=re.IGNORECASE)
# Remove any remaining parentheses and their contents
title = re.sub(r'\([^)]*\)', '', title)
# Remove common filler words
# filler_words = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'by']
title_words = title.split()
# title_words = [word for word in title_words if word.lower() not in filler_words]
title = ' '.join(title_words)
# Remove any extra whitespace
title = ' '.join(title.split())
logging.info(f"Cleaned Title: {title}")
return title.strip()

View File

@ -1,31 +1,52 @@
from src.clients.emby_client import EmbyClient
from services.playlist_service import PlaylistService
# codebase/emby-scripts/src/main.py
import logging
from src.config import Config
from src.clients.emby_client import EmbyClient
from src.clients.spotify_client import SpotifyClient
from src.services.playlist_service import PlaylistService
from src.actions.sync_spotify_playlists import sync_spotify_playlists
from src.actions.find_unmatched_songs import find_unmatched_songs
from src.actions.delete_duplicate_playlists import delete_duplicate_playlists
from src.actions.copy_emby_playlist_to_users import copy_playlists_to_users
from dotenv import load_dotenv
# Create an instance of the Emby client for the source user
source_emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Create an array of target usernames
target_usernames = ["Alyssa", "Dalton", "Laura", "Zuko", "Azula","Brittany","Adam","Chris"]
# Load environment variables from .env file
load_dotenv()
# Get all the playlists for the source user
playlists = source_emby.get_playlists()
def main():
# Initialize clients
emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
spotify = SpotifyClient(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET, Config.SPOTIFY_REDIRECT_URI, Config.SPOTIFY_SCOPE)
# 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, '')
# 1. Sync Spotify playlists
logger.info("Starting Spotify playlist sync...")
sync_spotify_playlists(spotify, emby)
logger.info("Spotify playlist sync completed.")
# Create an instance of the PlaylistService
playlist_service = PlaylistService(source_emby, target_emby)
# 2. Find unmatched songs
logger.info("Finding unmatched songs...")
find_unmatched_songs()
logger.info("Unmatched songs search completed.")
# 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)}")
# 3. Sync Spotify playlists again
logger.info("Starting second Spotify playlist sync...")
sync_spotify_playlists(spotify, emby)
logger.info("Second Spotify playlist sync completed.")
# 4. Delete duplicate playlists
logger.info("Deleting duplicate playlists...")
delete_duplicate_playlists(emby)
logger.info("Duplicate playlists deletion completed.")
# 5. Copy playlists to other users
logger.info("Copying playlists to other users...")
copy_playlists_to_users(emby)
logger.info("Playlist copying completed.")
if __name__ == "__main__":
main()

BIN
src/unmatched_songs.db Normal file

Binary file not shown.

0
src/unmatched_songs.log Normal file
View File