Skip to content

Commit 38ef78a

Browse files
authored
2.1.0
Merge pull request #37 from Gyarbij/dev
2 parents 9836e4f + 6ffb285 commit 38ef78a

File tree

8 files changed

+133
-126
lines changed

8 files changed

+133
-126
lines changed

Dockerfile

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
# Base image: https://hub.docker.com/_/python
2-
FROM python:3.12-rc-slim
1+
FROM python:3.12.0-slim
32

4-
# Prevents Python from generating .pyc files in the container
53
ENV PYTHONDONTWRITEBYTECODE=1
64

7-
# Turns off buffering for easier container logging
85
ENV PYTHONUNBUFFERED=1
96

10-
# Install pip requirements
7+
RUN apt-get update && apt-get install -y \
8+
gcc \
9+
libffi-dev \
10+
build-essential \
11+
&& rm -rf /var/lib/apt/lists/*
12+
1113
COPY requirements.txt .
12-
RUN python -m pip install -r requirements.txt
14+
RUN python -m pip install --no-cache-dir -r requirements.txt
1315

1416
WORKDIR /app
1517
COPY . /app
1618

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

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

24-
# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t gyarbij/plexist:<tag> --push .
24+
# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t gyarbij/plexist:<tag> --push .

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ docker run -d \
7676
7777
```
7878
#### Notes
79-
- Include `http://` in the PLEX_URL
79+
- Include `http://` or `https://` in the PLEX_URL
8080
- Remove comments (e.g. `# Optional x`) before running
8181

8282
### Docker Compose

SECURITY.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Supported Versions
44

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

77

88
## Reporting a Vulnerability

plexist/modules/deezer.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,15 @@ def _get_dz_tracks_from_playlist(
8080
List[Track]: list of Track objects with track metadata fields
8181
"""
8282

83-
def extract_dz_track_metadata(track):
84-
track = track.as_dict()
85-
title = track["title"]
86-
artist = track["artist"]["name"]
87-
album = track["album"]["title"]
88-
url = track.get("link", "")
89-
return Track(title, artist, album, url)
90-
91-
dz_playlist_tracks = dz.get_playlist(playlist.id).tracks
92-
93-
return list(map(extract_dz_track_metadata, dz_playlist_tracks))
83+
def extract_dz_track_metadata(track):
84+
track = track.as_dict()
85+
title = track["title"]
86+
artist = track["artist"]["name"]
87+
album = track["album"]["title"]
88+
year = track["album"].get("release_date", "").split("-")[0] # Assuming the release_date is in YYYY-MM-DD format
89+
genre = track["album"].get("genre_id", "")
90+
url = track.get("link", "")
91+
return Track(title, artist, album, url, year, genre) # Assuming Track class is modified to include year and genre
9492

9593

9694
def deezer_playlist_sync(

plexist/modules/plex.py

+93-92
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import csv
1+
import sqlite3
22
import logging
33
import pathlib
44
import sys
55
from difflib import SequenceMatcher
66
from typing import List
7-
7+
from concurrent.futures import ThreadPoolExecutor
88
import plexapi
99
from plexapi.exceptions import BadRequest, NotFound
1010
from plexapi.server import PlexServer
@@ -13,17 +13,48 @@
1313

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

16+
# Get connection object globally
17+
conn = sqlite3.connect('matched_songs.db')
1618

17-
def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
18-
"""Write given tracks with given name as a csv.
19+
# Database functions
20+
def initialize_db():
21+
cursor = conn.cursor()
22+
cursor.execute('''
23+
CREATE TABLE IF NOT EXISTS matched_songs (
24+
title TEXT,
25+
artist TEXT,
26+
album TEXT,
27+
year INTEGER,
28+
genre TEXT,
29+
plex_id INTEGER
30+
)
31+
''')
32+
conn.commit()
33+
34+
def insert_matched_song(title, artist, album, plex_id):
35+
cursor = conn.cursor()
36+
37+
cursor.execute('''
38+
INSERT INTO matched_songs (title, artist, album, plex_id)
39+
VALUES (?, ?, ?, ?)
40+
''', (title, artist, album, plex_id))
41+
42+
conn.commit()
43+
44+
def get_matched_song(title, artist, album):
45+
cursor = conn.cursor()
1946

20-
Args:
21-
tracks (List[Track]): List of Track objects
22-
name (str): Name of the file to write
23-
path (str): Root directory to write the file
24-
"""
25-
# pathlib.Path(path).mkdir(parents=True, exist_ok=True)
47+
cursor.execute('''
48+
SELECT plex_id FROM matched_songs
49+
WHERE title = ? AND artist = ? AND album = ?
50+
''', (title, artist, album))
2651

52+
result = cursor.fetchone()
53+
54+
return result[0] if result else None
55+
56+
57+
def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
2758
data_folder = pathlib.Path(path)
2859
data_folder.mkdir(parents=True, exist_ok=True)
2960
file = data_folder / f"{name}.csv"
@@ -38,95 +69,70 @@ def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
3869

3970

4071
def _delete_csv(name: str, path: str = "/data") -> None:
41-
"""Delete file associated with given name
42-
43-
Args:
44-
name (str): Name of the file to delete
45-
path (str, optional): Root directory to delete the file from
46-
"""
4772
data_folder = pathlib.Path(path)
4873
file = data_folder / f"{name}.csv"
4974
file.unlink()
5075

5176

77+
from concurrent.futures import ThreadPoolExecutor
78+
5279
def _get_available_plex_tracks(plex: PlexServer, tracks: List[Track]) -> List:
53-
"""Search and return list of tracks available in plex.
54-
55-
Args:
56-
plex (PlexServer): A configured PlexServer instance
57-
tracks (List[Track]): list of track objects
58-
59-
Returns:
60-
List: of plex track objects
61-
"""
62-
plex_tracks, missing_tracks = [], []
63-
for track in tracks:
64-
search = []
65-
try:
66-
search = plex.search(track.title, mediatype="track", limit=5)
67-
except BadRequest:
68-
logging.info("failed to search %s on plex", track.title)
69-
if (not search) or len(track.title.split("(")) > 1:
70-
logging.info("retrying search for %s", track.title)
71-
try:
72-
search += plex.search(
73-
track.title.split("(")[0], mediatype="track", limit=5
74-
)
75-
logging.info("search for %s successful", track.title)
76-
except BadRequest:
77-
logging.info("unable to query %s on plex", track.title)
78-
79-
found = False
80-
if search:
81-
for s in search:
82-
try:
83-
artist_similarity = SequenceMatcher(
84-
None, s.artist().title.lower(), track.artist.lower()
85-
).quick_ratio()
86-
87-
if artist_similarity >= 0.9:
88-
plex_tracks.extend(s)
89-
found = True
90-
break
91-
92-
album_similarity = SequenceMatcher(
93-
None, s.album().title.lower(), track.album.lower()
94-
).quick_ratio()
95-
96-
if album_similarity >= 0.9:
97-
plex_tracks.extend(s)
98-
found = True
99-
break
100-
101-
except IndexError:
102-
logging.info(
103-
"Looks like plex mismatched the search for %s,"
104-
" retrying with next result",
105-
track.title,
106-
)
107-
if not found:
108-
missing_tracks.append(track)
80+
with ThreadPoolExecutor() as executor:
81+
results = list(executor.map(lambda track: _match_single_track(plex, track), tracks))
82+
83+
plex_tracks = [result[0] for result in results if result[0]]
84+
missing_tracks = [result[1] for result in results if result[1]]
10985

11086
return plex_tracks, missing_tracks
11187

88+
MATCH_THRESHOLD = 0.8 # Set your own threshold
89+
90+
def _match_single_track(plex, track, year=None, genre=None):
91+
# Check in local DB first
92+
plex_id = get_matched_song(track.title, track.artist, track.album)
93+
if plex_id:
94+
return plex.fetchItem(plex_id), None
95+
96+
search = []
97+
try:
98+
# Combine track title, artist, and album for a more refined search
99+
search_query = f"{track.title} {track.artist} {track.album}"
100+
search = plex.search(search_query, mediatype="track", limit=5)
101+
except BadRequest:
102+
logging.info("Failed to search %s on Plex", track.title)
103+
104+
best_match = None
105+
best_score = 0
106+
107+
for s in search:
108+
artist_similarity = SequenceMatcher(None, s.artist().title.lower(), track.artist.lower()).quick_ratio()
109+
title_similarity = SequenceMatcher(None, s.title.lower(), track.title.lower()).quick_ratio()
110+
album_similarity = SequenceMatcher(None, s.album().title.lower(), track.album.lower()).quick_ratio()
111+
year_similarity = 1 if year and s.year == year else 0
112+
genre_similarity = SequenceMatcher(None, s.genre.lower(), genre.lower()).quick_ratio() if genre else 0
113+
114+
# Combine the scores (you can adjust the weights as needed)
115+
combined_score = (artist_similarity * 0.4) + (title_similarity * 0.3) + (album_similarity * 0.2) + (year_similarity * 0.05) + (genre_similarity * 0.05)
116+
117+
if combined_score > best_score:
118+
best_score = combined_score
119+
best_match = s
120+
121+
if best_match and best_score >= MATCH_THRESHOLD:
122+
# Insert into the local DB
123+
insert_matched_song(track.title, track.artist, track.album, best_match.ratingKey)
124+
return best_match, None
125+
else:
126+
logging.info(f"No match found for track {track.title} by {track.artist} with a score of {best_score}.")
127+
return None, track
128+
112129

113130
def _update_plex_playlist(
114131
plex: PlexServer,
115132
available_tracks: List,
116133
playlist: Playlist,
117134
append: bool = False,
118135
) -> plexapi.playlist.Playlist:
119-
"""Update existing plex playlist with new tracks and metadata.
120-
121-
Args:
122-
plex (PlexServer): A configured PlexServer instance
123-
available_tracks (List): list of plex track objects
124-
playlist (Playlist): Playlist object
125-
append (bool): Boolean for Append or sync
126-
127-
Returns:
128-
plexapi.playlist.Playlist: plex playlist object
129-
"""
130136
plex_playlist = plex.playlist(playlist.name)
131137
if not append:
132138
plex_playlist.removeItems(plex_playlist.items())
@@ -140,13 +146,6 @@ def update_or_create_plex_playlist(
140146
tracks: List[Track],
141147
userInputs: UserInputs,
142148
) -> None:
143-
"""Update playlist if exists, else create a new playlist.
144-
145-
Args:
146-
plex (PlexServer): A configured PlexServer instance
147-
available_tracks (List): List of plex.audio.track objects
148-
playlist (Playlist): Playlist object
149-
"""
150149
available_tracks, missing_tracks = _get_available_plex_tracks(plex, tracks)
151150
if available_tracks:
152151
try:
@@ -199,11 +198,13 @@ def update_or_create_plex_playlist(
199198
)
200199
if (not missing_tracks) and userInputs.write_missing_as_csv:
201200
try:
202-
# Delete playlist created in prev run if no tracks are missing now
203201
_delete_csv(playlist.name)
204202
logging.info("Deleted old %s.csv", playlist.name)
205203
except:
206204
logging.info(
207205
"Failed to delete %s.csv, likely permission issue",
208206
playlist.name,
209-
)
207+
)
208+
209+
def end_session():
210+
conn.close()

plexist/modules/spotify.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,17 @@ def extract_sp_track_metadata(track) -> Track:
7171
def spotify_playlist_sync(
7272
sp: spotipy.Spotify, plex: PlexServer, userInputs: UserInputs
7373
) -> None:
74-
playlists = _get_sp_user_playlists(sp, userInputs.spotify_user_id)
75-
if playlists:
76-
for playlist in playlists:
77-
tracks = _get_sp_tracks_from_playlist(
78-
sp, userInputs.spotify_user_id, playlist
79-
)
80-
update_or_create_plex_playlist(plex, playlist, tracks, userInputs)
81-
else:
82-
logging.error("No spotify playlists found for user provided")
74+
try:
75+
playlists = _get_sp_user_playlists(sp, userInputs.spotify_user_id)
76+
if playlists:
77+
for playlist in playlists:
78+
logging.info(f"Syncing playlist: {playlist.name}")
79+
tracks = _get_sp_tracks_from_playlist(
80+
sp, userInputs.spotify_user_id, playlist
81+
)
82+
# Pass additional metadata like year and genre if available
83+
update_or_create_plex_playlist(plex, playlist, tracks, userInputs)
84+
else:
85+
logging.error("No Spotify playlists found for the user provided.")
86+
except spotipy.SpotifyException as e:
87+
logging.error(f"Spotify Exception: {e}")

plexist/plexist.py

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from modules.deezer import deezer_playlist_sync
1313
from modules.helperClasses import UserInputs
1414
from modules.spotify import spotify_playlist_sync
15+
from modules.plex import initialize_db # Importing the database initialization function
1516

1617

1718
def read_environment_variables():
@@ -65,6 +66,8 @@ def initialize_spotify_client(user_inputs):
6566

6667

6768
def main():
69+
initialize_db() # Initialize the database at the start of the main function
70+
6871
user_inputs = read_environment_variables()
6972
plex = initialize_plex_server(user_inputs)
7073

requirements.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
spotipy>=2.22.1
2-
plexapi>=4.13.4
3-
deezer-python>=5.8.1
1+
spotipy>=2.23
2+
plexapi>=4.15.4
3+
deezer-python>=6.1.0

0 commit comments

Comments
 (0)