feature: added dockerfile and docker-entrypoint.sh
This commit is contained in:
parent
62fc4201ca
commit
c0cb7ecbee
|
|
@ -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"]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 2 * * * /usr/local/bin/python /app/src/main.py >> /var/log/cron.log 2>&1
|
||||||
|
|
@ -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
|
||||||
Binary file not shown.
|
|
@ -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"}
|
||||||
|
|
@ -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.
|
||||||
|
-
|
||||||
|
|
||||||
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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)}")
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -2,11 +2,22 @@ import shlex
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import unicodedata
|
||||||
import re
|
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
|
# Configure logging
|
||||||
logging.basicConfig(
|
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
|
# 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
|
# Set the output directory where the songs will be saved
|
||||||
output_dir = "C:\\Users\\Dane Miller\\Music\\spotdl_downloads"
|
output_dir = "C:\\Users\\Dane Miller\\Music\\spotdl_downloads"
|
||||||
|
|
||||||
# Connect to the SQLite database
|
def remove_special_characters(string, skip_non_ascii=False):
|
||||||
conn = sqlite3.connect('unmatched_songs.db')
|
if skip_non_ascii and not all(ord(char) < 128 for char in string):
|
||||||
c = conn.cursor()
|
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
|
return cleaned_string
|
||||||
|
|
||||||
# Create the table to store downloaded songs if it doesn't exist
|
def find_unmatched_songs():
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS downloaded_songs
|
# Connect to the SQLite database
|
||||||
(track_name TEXT, artist_name TEXT, album_name TEXT)''')
|
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
|
# Remove duplicates from the unmatched songs table
|
||||||
c.execute('''DELETE FROM unmatched_songs
|
c.execute('''DELETE FROM unmatched_songs
|
||||||
WHERE rowid NOT IN (
|
WHERE rowid NOT IN (
|
||||||
SELECT MIN(rowid)
|
SELECT MIN(rowid)
|
||||||
FROM unmatched_songs
|
FROM unmatched_songs
|
||||||
GROUP BY track_name, artist_name, album_name
|
GROUP BY track_name, artist_name, album_name
|
||||||
)''')
|
)''')
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Retrieve the unmatched songs from the database
|
# Retrieve the unmatched songs from the database
|
||||||
c.execute('SELECT DISTINCT track_name, artist_name, album_name FROM unmatched_songs')
|
c.execute('SELECT DISTINCT track_name, artist_name, album_name FROM unmatched_songs')
|
||||||
unmatched_songs = c.fetchall()
|
unmatched_songs = c.fetchall()
|
||||||
|
|
||||||
# Get the total number of unmatched songs
|
# Get the total number of unmatched songs
|
||||||
total_unmatched_songs = len(unmatched_songs)
|
total_unmatched_songs = len(unmatched_songs)
|
||||||
|
|
||||||
# Process each unmatched song
|
# Process each unmatched song
|
||||||
for index, song in enumerate(unmatched_songs, start=1):
|
for index, song in enumerate(unmatched_songs, start=1):
|
||||||
track_name, artist_name, album_name = song
|
track_name, artist_name, album_name = song
|
||||||
|
|
||||||
# Check if the song has already been downloaded
|
cleaned_track = remove_special_characters(track_name)
|
||||||
c.execute('SELECT COUNT(*) FROM downloaded_songs WHERE track_name = ? AND artist_name = ? AND album_name = ?',
|
cleaned_artist = remove_special_characters(artist_name)
|
||||||
(track_name, artist_name, album_name))
|
|
||||||
count = c.fetchone()[0]
|
|
||||||
|
|
||||||
if count > 0:
|
# Check if the song has already been downloaded
|
||||||
logging.info(f"Skipping '{track_name}' by {artist_name} - already downloaded")
|
c.execute('SELECT COUNT(*) FROM downloaded_songs WHERE track_name = ? AND artist_name = ? AND album_name = ?',
|
||||||
continue
|
(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
|
logging.info(f"Processing unmatched song: '{track_name}' by {artist_name}")
|
||||||
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}")
|
|
||||||
|
|
||||||
# Insert the downloaded song into the database
|
# Use spotdl to find and download the song
|
||||||
c.execute('INSERT INTO downloaded_songs VALUES (?, ?, ?)', (track_name, artist_name, album_name))
|
try:
|
||||||
conn.commit()
|
command = f'"{spotdl_path}" download "{cleaned_artist} - {cleaned_track}" --output "{output_dir}"'
|
||||||
except subprocess.CalledProcessError as e:
|
logging.info(f"Running command: {command}")
|
||||||
logging.error(f"Error downloading '{track_name}' by {artist_name}")
|
subprocess.run(command, shell=True, check=True)
|
||||||
logging.error(f"Error message: {str(e)}")
|
logging.info(f"Successfully downloaded '{track_name}' by {artist_name}")
|
||||||
|
|
||||||
# Calculate the percentage of songs processed
|
# Insert the downloaded song into the database
|
||||||
percentage = (index / total_unmatched_songs) * 100
|
c.execute('INSERT INTO downloaded_songs VALUES (?, ?, ?)', (track_name, artist_name, album_name))
|
||||||
logging.info(f"Progress: {index}/{total_unmatched_songs} ({percentage:.2f}%)")
|
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
|
# Calculate the percentage of songs processed
|
||||||
conn.close()
|
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()
|
||||||
|
|
@ -2,12 +2,13 @@ import spotipy
|
||||||
from spotipy.oauth2 import SpotifyOAuth
|
from spotipy.oauth2 import SpotifyOAuth
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from fuzzywuzzy import fuzz
|
from fuzzywuzzy import fuzz
|
||||||
import re
|
import re
|
||||||
from src.config import Config
|
from src.config import Config
|
||||||
from src.utils.string import StringUtils
|
from src.utils.string import StringUtils
|
||||||
from src.clients.emby_client import EmbyClient
|
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
|
import sqlite3
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
|
|
@ -16,137 +17,145 @@ logging.basicConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the user's playlists from Spotify
|
# Get the user's playlists from Spotify
|
||||||
spot = SpotifyClient(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET, Config.SPOTIFY_REDIRECT_URI,
|
# spot = SpotifyClient(Config.SPOTIFY_CLIENT_ID, Config.SPOTIFY_CLIENT_SECRET, Config.SPOTIFY_REDIRECT_URI,
|
||||||
Config.SPOTIFY_SCOPE)
|
# Config.SPOTIFY_SCOPE)
|
||||||
emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
# 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)
|
# 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:
|
conn = sqlite3.connect('unmatched_songs.db')
|
||||||
logging.error(f"Error creating Emby playlist: {playlist_name}")
|
c = conn.cursor()
|
||||||
logging.error(f"Error message: {str(e)}")
|
# 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
|
playlists = spot.get_playlists()
|
||||||
tracks = spot.get_playlist_tracks(playlist_id)
|
featured_playlists = spot.get_featured_playlists()
|
||||||
|
# print(json.dumps(featured_playlists, indent=4))
|
||||||
|
|
||||||
logging.info(f"Processing {len(tracks)} tracks in Spotify playlist")
|
all_playlists = playlists["items"] + featured_playlists["items"]
|
||||||
# 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)
|
|
||||||
|
|
||||||
emby_search_results = emby.search_for_track(track_name, artist_name)
|
# Iterate over each Spotify playlist
|
||||||
emby_search_results_cleaned = emby.search_for_track(
|
for playlist in all_playlists:
|
||||||
clean_track_name, clean_artist_name
|
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:
|
emby_playlist_name = f"{playlist_name} ({playlist_owner})"
|
||||||
found_match = False
|
|
||||||
for result in emby_search_results:
|
|
||||||
if found_match:
|
|
||||||
break
|
|
||||||
|
|
||||||
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 emby_playlist_search_results:
|
||||||
if spot.match_song(track["track"], result):
|
# Delete the existing playlist
|
||||||
logging.debug(f"Matched track: {track_name}")
|
existing_playlist_id = emby_playlist_search_results[0]["Id"]
|
||||||
try:
|
|
||||||
emby.add_item_to_playlist(emby_playlist['Id'], emby_item_id)
|
for existing_playlist in emby_playlist_search_results:
|
||||||
logging.info(
|
if (
|
||||||
f"Added '{track_name}' by {artist_name} to Emby playlist"
|
existing_playlist["Name"] == emby_playlist_name
|
||||||
)
|
and existing_playlist["Type"] == "Playlist"
|
||||||
found_match = True
|
):
|
||||||
added_tracks += 1
|
emby.delete_playlist(existing_playlist['Id'])
|
||||||
except requests.exceptions.RequestException as e:
|
logging.info(
|
||||||
logging.warning(
|
f"Deleted existing Emby playlist: {emby_playlist_name} (ID: {existing_playlist_id})"
|
||||||
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(
|
# Create a new playlist in Emby
|
||||||
f"EMBY: {result['Name']} by {result.get('Artists', [])}"
|
try:
|
||||||
)
|
emby_playlist = emby.create_playlist(emby_playlist_name, 'Audio')
|
||||||
else:
|
logging.info(
|
||||||
logging.warning(
|
f"Created Emby playlist: {playlist_name} (ID: {emby_playlist["Id"]})"
|
||||||
f"No match found for '{track_name}' by {artist_name} in Emby"
|
|
||||||
)
|
)
|
||||||
|
# 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))
|
unmatched_tracks.append((playlist_name, track_name, artist_name, album_name))
|
||||||
|
|
||||||
# Insert the unmatched songs into the database
|
# Insert the unmatched songs into the database
|
||||||
c.executemany('INSERT INTO unmatched_songs VALUES (?, ?, ?, ?)', unmatched_tracks)
|
c.executemany('INSERT INTO unmatched_songs VALUES (?, ?, ?, ?)', unmatched_tracks)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Calculate the match percentage for the current playlist
|
# Calculate the match percentage for the current playlist
|
||||||
if len(tracks) > 0:
|
if len(tracks) > 0:
|
||||||
match_percentage = (added_tracks / len(tracks)) * 100
|
match_percentage = (added_tracks / len(tracks)) * 100
|
||||||
logging.info(f"Match percentage for playlist '{playlist_name}': {match_percentage:.2f}%")
|
logging.info(f"Match percentage for playlist '{playlist_name}': {match_percentage:.2f}%")
|
||||||
|
|
||||||
added_tracks = 0
|
added_tracks = 0
|
||||||
|
|
||||||
# Close the database connection
|
# Close the database connection
|
||||||
conn.close()
|
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
Binary file not shown.
|
|
@ -1,9 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import requests
|
import requests
|
||||||
import spotipy
|
import spotipy
|
||||||
from fuzzywuzzy import fuzz
|
from fuzzywuzzy import fuzz
|
||||||
from spotipy import SpotifyOAuth
|
from spotipy import SpotifyOAuth
|
||||||
|
|
||||||
from src.utils.string import StringUtils
|
from src.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,9 +25,13 @@ class SpotifyClient:
|
||||||
Match Spotify artists with Emby artists using fuzzy string matching.
|
Match Spotify artists with Emby artists using fuzzy string matching.
|
||||||
Returns the maximum artist similarity score found.
|
Returns the maximum artist similarity score found.
|
||||||
"""
|
"""
|
||||||
|
# print("MATCH ARTISTS")
|
||||||
|
# print(spotify_artists)
|
||||||
|
# print(emby_artists)
|
||||||
|
|
||||||
max_artist_score = 0
|
max_artist_score = 0
|
||||||
for spotify_artist in spotify_artists:
|
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:
|
for emby_artist in emby_artists:
|
||||||
emby_artist_name = StringUtils.clean_string(emby_artist)
|
emby_artist_name = StringUtils.clean_string(emby_artist)
|
||||||
artist_score = fuzz.ratio(spotify_artist_name, emby_artist_name)
|
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.
|
Match a Spotify song with an Emby song using fuzzy string matching.
|
||||||
"""
|
"""
|
||||||
|
# print(spotify_song)
|
||||||
|
# print(emby_song)
|
||||||
try:
|
try:
|
||||||
spotify_title = StringUtils.clean_string(spotify_song["name"])
|
spotify_title = StringUtils.clean_string(spotify_song.get("name", "").lower())
|
||||||
spotify_artists = spotify_song["artists"]
|
spotify_artists = [artist.get("name", "").lower() for artist in spotify_song["artists"]]
|
||||||
|
|
||||||
emby_title = StringUtils.clean_string(emby_song["Name"])
|
emby_title = StringUtils.clean_string(emby_song.get("Name", "").lower())
|
||||||
emby_artists = emby_song["Artists"]
|
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)
|
artist_ratio = self.match_artists(spotify_artists, emby_artists)
|
||||||
|
|
||||||
|
# print("ARTIST RATIO", artist_ratio)
|
||||||
|
|
||||||
# Adjust the thresholds as needed
|
# Adjust the thresholds as needed
|
||||||
title_threshold = 90
|
title_threshold = 85
|
||||||
artist_threshold = 80
|
artist_threshold = 75
|
||||||
combined_threshold = 170 # Sum of title and artist thresholds
|
combined_threshold = 165 # Sum of title and artist thresholds
|
||||||
|
|
||||||
if title_ratio >= title_threshold and artist_ratio >= artist_threshold:
|
if title_ratio >= title_threshold and artist_ratio >= artist_threshold:
|
||||||
combined_score = title_ratio + artist_ratio
|
combined_score = title_ratio + artist_ratio
|
||||||
if combined_score >= combined_threshold:
|
if combined_score >= combined_threshold:
|
||||||
|
print(f"MATCH: {spotify_title} by {spotify_artists} vs {emby_title} by {emby_artists}")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
||||||
logging.error(f"Error matching song: {str(e)}")
|
logging.error(f"Error matching song: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -85,6 +112,7 @@ class SpotifyClient:
|
||||||
return image_data
|
return image_data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_liked_songs(self):
|
def get_liked_songs(self):
|
||||||
liked_songs = []
|
liked_songs = []
|
||||||
results = self.sp.current_user_saved_tracks()
|
results = self.sp.current_user_saved_tracks()
|
||||||
|
|
@ -92,3 +120,75 @@ class SpotifyClient:
|
||||||
liked_songs.extend(results["items"])
|
liked_songs.extend(results["items"])
|
||||||
results = self.sp.next(results)
|
results = self.sp.next(results)
|
||||||
return liked_songs
|
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()
|
||||||
|
|
|
||||||
69
src/main.py
69
src/main.py
|
|
@ -1,31 +1,52 @@
|
||||||
from src.clients.emby_client import EmbyClient
|
# codebase/emby-scripts/src/main.py
|
||||||
from services.playlist_service import PlaylistService
|
|
||||||
|
import logging
|
||||||
from src.config import Config
|
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
|
# Configure logging
|
||||||
source_emby = EmbyClient(Config.EMBY_URL, Config.EMBY_USERNAME, Config.EMBY_PASSWORD)
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Create an array of target usernames
|
# Load environment variables from .env file
|
||||||
target_usernames = ["Alyssa", "Dalton", "Laura", "Zuko", "Azula","Brittany","Adam","Chris"]
|
load_dotenv()
|
||||||
|
|
||||||
# Get all the playlists for the source user
|
def main():
|
||||||
playlists = source_emby.get_playlists()
|
# 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
|
# 1. Sync Spotify playlists
|
||||||
for playlist in playlists:
|
logger.info("Starting Spotify playlist sync...")
|
||||||
# Iterate over the target usernames
|
sync_spotify_playlists(spotify, emby)
|
||||||
for target_username in target_usernames:
|
logger.info("Spotify playlist sync completed.")
|
||||||
# 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
|
# 2. Find unmatched songs
|
||||||
playlist_service = PlaylistService(source_emby, target_emby)
|
logger.info("Finding unmatched songs...")
|
||||||
|
find_unmatched_songs()
|
||||||
|
logger.info("Unmatched songs search completed.")
|
||||||
|
|
||||||
# Copy the playlist to the target user
|
# 3. Sync Spotify playlists again
|
||||||
new_playlist = playlist_service.copy_playlist_by_usernames(Config.EMBY_USERNAME, target_username, playlist["Id"])
|
logger.info("Starting second Spotify playlist sync...")
|
||||||
print(f"Playlist '{playlist['Name']}' copied successfully to user '{target_username}'. New playlist ID: {new_playlist['Id']}")
|
sync_spotify_playlists(spotify, emby)
|
||||||
except ValueError as e:
|
logger.info("Second Spotify playlist sync completed.")
|
||||||
print(f"Error copying playlist '{playlist['Name']}' to user '{target_username}': {str(e)}")
|
|
||||||
|
# 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()
|
||||||
Binary file not shown.
Loading…
Reference in New Issue