Skip to content

Commit

Permalink
2.1.0
Browse files Browse the repository at this point in the history
Merge pull request #37 from Gyarbij/dev
  • Loading branch information
Gyarbij authored Oct 12, 2023
2 parents 9836e4f + 6ffb285 commit 38ef78a
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 126 deletions.
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# Base image: https://hub.docker.com/_/python
FROM python:3.12-rc-slim
FROM python:3.12.0-slim

# Prevents Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE=1

# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED=1

# Install pip requirements
RUN apt-get update && apt-get install -y \
gcc \
libffi-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN python -m pip install -r requirements.txt
RUN python -m pip install --no-cache-dir -r requirements.txt

WORKDIR /app
COPY . /app

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
RUN adduser -u 5678 --disabled-password --gecos "" plexist && chown -R plexist /app
USER plexist

# During debugging, this entry point will be overridden.
CMD ["python", "plexist/plexist.py"]

# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t gyarbij/plexist:<tag> --push .
# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t gyarbij/plexist:<tag> --push .
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ docker run -d \
```
#### Notes
- Include `http://` in the PLEX_URL
- Include `http://` or `https://` in the PLEX_URL
- Remove comments (e.g. `# Optional x`) before running

### Docker Compose
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Supported Versions

The only version supported is the current builds located at [Docker Hub](https://hub.docker.com/r/gyarbij/wireui/tags)
The only version supported is the current tag:latest build located at [Docker Hub](https://hub.docker.com/r/gyarbij/wireui/tags)


## Reporting a Vulnerability
Expand Down
20 changes: 9 additions & 11 deletions plexist/modules/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,15 @@ def _get_dz_tracks_from_playlist(
List[Track]: list of Track objects with track metadata fields
"""

def extract_dz_track_metadata(track):
track = track.as_dict()
title = track["title"]
artist = track["artist"]["name"]
album = track["album"]["title"]
url = track.get("link", "")
return Track(title, artist, album, url)

dz_playlist_tracks = dz.get_playlist(playlist.id).tracks

return list(map(extract_dz_track_metadata, dz_playlist_tracks))
def extract_dz_track_metadata(track):
track = track.as_dict()
title = track["title"]
artist = track["artist"]["name"]
album = track["album"]["title"]
year = track["album"].get("release_date", "").split("-")[0] # Assuming the release_date is in YYYY-MM-DD format
genre = track["album"].get("genre_id", "")
url = track.get("link", "")
return Track(title, artist, album, url, year, genre) # Assuming Track class is modified to include year and genre


def deezer_playlist_sync(
Expand Down
185 changes: 93 additions & 92 deletions plexist/modules/plex.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import csv
import sqlite3
import logging
import pathlib
import sys
from difflib import SequenceMatcher
from typing import List

from concurrent.futures import ThreadPoolExecutor
import plexapi
from plexapi.exceptions import BadRequest, NotFound
from plexapi.server import PlexServer
Expand All @@ -13,17 +13,48 @@

logging.basicConfig(stream=sys.stdout, level=logging.INFO)

# Get connection object globally
conn = sqlite3.connect('matched_songs.db')

def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
"""Write given tracks with given name as a csv.
# Database functions
def initialize_db():
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS matched_songs (
title TEXT,
artist TEXT,
album TEXT,
year INTEGER,
genre TEXT,
plex_id INTEGER
)
''')
conn.commit()

def insert_matched_song(title, artist, album, plex_id):
cursor = conn.cursor()

cursor.execute('''
INSERT INTO matched_songs (title, artist, album, plex_id)
VALUES (?, ?, ?, ?)
''', (title, artist, album, plex_id))

conn.commit()

def get_matched_song(title, artist, album):
cursor = conn.cursor()

Args:
tracks (List[Track]): List of Track objects
name (str): Name of the file to write
path (str): Root directory to write the file
"""
# pathlib.Path(path).mkdir(parents=True, exist_ok=True)
cursor.execute('''
SELECT plex_id FROM matched_songs
WHERE title = ? AND artist = ? AND album = ?
''', (title, artist, album))

result = cursor.fetchone()

return result[0] if result else None


def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
data_folder = pathlib.Path(path)
data_folder.mkdir(parents=True, exist_ok=True)
file = data_folder / f"{name}.csv"
Expand All @@ -38,95 +69,70 @@ def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:


def _delete_csv(name: str, path: str = "/data") -> None:
"""Delete file associated with given name
Args:
name (str): Name of the file to delete
path (str, optional): Root directory to delete the file from
"""
data_folder = pathlib.Path(path)
file = data_folder / f"{name}.csv"
file.unlink()


from concurrent.futures import ThreadPoolExecutor

def _get_available_plex_tracks(plex: PlexServer, tracks: List[Track]) -> List:
"""Search and return list of tracks available in plex.
Args:
plex (PlexServer): A configured PlexServer instance
tracks (List[Track]): list of track objects
Returns:
List: of plex track objects
"""
plex_tracks, missing_tracks = [], []
for track in tracks:
search = []
try:
search = plex.search(track.title, mediatype="track", limit=5)
except BadRequest:
logging.info("failed to search %s on plex", track.title)
if (not search) or len(track.title.split("(")) > 1:
logging.info("retrying search for %s", track.title)
try:
search += plex.search(
track.title.split("(")[0], mediatype="track", limit=5
)
logging.info("search for %s successful", track.title)
except BadRequest:
logging.info("unable to query %s on plex", track.title)

found = False
if search:
for s in search:
try:
artist_similarity = SequenceMatcher(
None, s.artist().title.lower(), track.artist.lower()
).quick_ratio()

if artist_similarity >= 0.9:
plex_tracks.extend(s)
found = True
break

album_similarity = SequenceMatcher(
None, s.album().title.lower(), track.album.lower()
).quick_ratio()

if album_similarity >= 0.9:
plex_tracks.extend(s)
found = True
break

except IndexError:
logging.info(
"Looks like plex mismatched the search for %s,"
" retrying with next result",
track.title,
)
if not found:
missing_tracks.append(track)
with ThreadPoolExecutor() as executor:
results = list(executor.map(lambda track: _match_single_track(plex, track), tracks))

plex_tracks = [result[0] for result in results if result[0]]
missing_tracks = [result[1] for result in results if result[1]]

return plex_tracks, missing_tracks

MATCH_THRESHOLD = 0.8 # Set your own threshold

def _match_single_track(plex, track, year=None, genre=None):
# Check in local DB first
plex_id = get_matched_song(track.title, track.artist, track.album)
if plex_id:
return plex.fetchItem(plex_id), None

search = []
try:
# Combine track title, artist, and album for a more refined search
search_query = f"{track.title} {track.artist} {track.album}"
search = plex.search(search_query, mediatype="track", limit=5)
except BadRequest:
logging.info("Failed to search %s on Plex", track.title)

best_match = None
best_score = 0

for s in search:
artist_similarity = SequenceMatcher(None, s.artist().title.lower(), track.artist.lower()).quick_ratio()
title_similarity = SequenceMatcher(None, s.title.lower(), track.title.lower()).quick_ratio()
album_similarity = SequenceMatcher(None, s.album().title.lower(), track.album.lower()).quick_ratio()
year_similarity = 1 if year and s.year == year else 0
genre_similarity = SequenceMatcher(None, s.genre.lower(), genre.lower()).quick_ratio() if genre else 0

# Combine the scores (you can adjust the weights as needed)
combined_score = (artist_similarity * 0.4) + (title_similarity * 0.3) + (album_similarity * 0.2) + (year_similarity * 0.05) + (genre_similarity * 0.05)

if combined_score > best_score:
best_score = combined_score
best_match = s

if best_match and best_score >= MATCH_THRESHOLD:
# Insert into the local DB
insert_matched_song(track.title, track.artist, track.album, best_match.ratingKey)
return best_match, None
else:
logging.info(f"No match found for track {track.title} by {track.artist} with a score of {best_score}.")
return None, track


def _update_plex_playlist(
plex: PlexServer,
available_tracks: List,
playlist: Playlist,
append: bool = False,
) -> plexapi.playlist.Playlist:
"""Update existing plex playlist with new tracks and metadata.
Args:
plex (PlexServer): A configured PlexServer instance
available_tracks (List): list of plex track objects
playlist (Playlist): Playlist object
append (bool): Boolean for Append or sync
Returns:
plexapi.playlist.Playlist: plex playlist object
"""
plex_playlist = plex.playlist(playlist.name)
if not append:
plex_playlist.removeItems(plex_playlist.items())
Expand All @@ -140,13 +146,6 @@ def update_or_create_plex_playlist(
tracks: List[Track],
userInputs: UserInputs,
) -> None:
"""Update playlist if exists, else create a new playlist.
Args:
plex (PlexServer): A configured PlexServer instance
available_tracks (List): List of plex.audio.track objects
playlist (Playlist): Playlist object
"""
available_tracks, missing_tracks = _get_available_plex_tracks(plex, tracks)
if available_tracks:
try:
Expand Down Expand Up @@ -199,11 +198,13 @@ def update_or_create_plex_playlist(
)
if (not missing_tracks) and userInputs.write_missing_as_csv:
try:
# Delete playlist created in prev run if no tracks are missing now
_delete_csv(playlist.name)
logging.info("Deleted old %s.csv", playlist.name)
except:
logging.info(
"Failed to delete %s.csv, likely permission issue",
playlist.name,
)
)

def end_session():
conn.close()
23 changes: 14 additions & 9 deletions plexist/modules/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,17 @@ def extract_sp_track_metadata(track) -> Track:
def spotify_playlist_sync(
sp: spotipy.Spotify, plex: PlexServer, userInputs: UserInputs
) -> None:
playlists = _get_sp_user_playlists(sp, userInputs.spotify_user_id)
if playlists:
for playlist in playlists:
tracks = _get_sp_tracks_from_playlist(
sp, userInputs.spotify_user_id, playlist
)
update_or_create_plex_playlist(plex, playlist, tracks, userInputs)
else:
logging.error("No spotify playlists found for user provided")
try:
playlists = _get_sp_user_playlists(sp, userInputs.spotify_user_id)
if playlists:
for playlist in playlists:
logging.info(f"Syncing playlist: {playlist.name}")
tracks = _get_sp_tracks_from_playlist(
sp, userInputs.spotify_user_id, playlist
)
# Pass additional metadata like year and genre if available
update_or_create_plex_playlist(plex, playlist, tracks, userInputs)
else:
logging.error("No Spotify playlists found for the user provided.")
except spotipy.SpotifyException as e:
logging.error(f"Spotify Exception: {e}")
3 changes: 3 additions & 0 deletions plexist/plexist.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from modules.deezer import deezer_playlist_sync
from modules.helperClasses import UserInputs
from modules.spotify import spotify_playlist_sync
from modules.plex import initialize_db # Importing the database initialization function


def read_environment_variables():
Expand Down Expand Up @@ -65,6 +66,8 @@ def initialize_spotify_client(user_inputs):


def main():
initialize_db() # Initialize the database at the start of the main function

user_inputs = read_environment_variables()
plex = initialize_plex_server(user_inputs)

Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
spotipy>=2.22.1
plexapi>=4.13.4
deezer-python>=5.8.1
spotipy>=2.23
plexapi>=4.15.4
deezer-python>=6.1.0

0 comments on commit 38ef78a

Please sign in to comment.