1
- import csv
1
+ import sqlite3
2
2
import logging
3
3
import pathlib
4
4
import sys
5
5
from difflib import SequenceMatcher
6
6
from typing import List
7
-
7
+ from concurrent . futures import ThreadPoolExecutor
8
8
import plexapi
9
9
from plexapi .exceptions import BadRequest , NotFound
10
10
from plexapi .server import PlexServer
13
13
14
14
logging .basicConfig (stream = sys .stdout , level = logging .INFO )
15
15
16
+ # Get connection object globally
17
+ conn = sqlite3 .connect ('matched_songs.db' )
16
18
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 ()
19
46
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 ))
26
51
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 :
27
58
data_folder = pathlib .Path (path )
28
59
data_folder .mkdir (parents = True , exist_ok = True )
29
60
file = data_folder / f"{ name } .csv"
@@ -38,95 +69,70 @@ def _write_csv(tracks: List[Track], name: str, path: str = "/data") -> None:
38
69
39
70
40
71
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
- """
47
72
data_folder = pathlib .Path (path )
48
73
file = data_folder / f"{ name } .csv"
49
74
file .unlink ()
50
75
51
76
77
+ from concurrent .futures import ThreadPoolExecutor
78
+
52
79
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 ]]
109
85
110
86
return plex_tracks , missing_tracks
111
87
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
+
112
129
113
130
def _update_plex_playlist (
114
131
plex : PlexServer ,
115
132
available_tracks : List ,
116
133
playlist : Playlist ,
117
134
append : bool = False ,
118
135
) -> 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
- """
130
136
plex_playlist = plex .playlist (playlist .name )
131
137
if not append :
132
138
plex_playlist .removeItems (plex_playlist .items ())
@@ -140,13 +146,6 @@ def update_or_create_plex_playlist(
140
146
tracks : List [Track ],
141
147
userInputs : UserInputs ,
142
148
) -> 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
- """
150
149
available_tracks , missing_tracks = _get_available_plex_tracks (plex , tracks )
151
150
if available_tracks :
152
151
try :
@@ -199,11 +198,13 @@ def update_or_create_plex_playlist(
199
198
)
200
199
if (not missing_tracks ) and userInputs .write_missing_as_csv :
201
200
try :
202
- # Delete playlist created in prev run if no tracks are missing now
203
201
_delete_csv (playlist .name )
204
202
logging .info ("Deleted old %s.csv" , playlist .name )
205
203
except :
206
204
logging .info (
207
205
"Failed to delete %s.csv, likely permission issue" ,
208
206
playlist .name ,
209
- )
207
+ )
208
+
209
+ def end_session ():
210
+ conn .close ()
0 commit comments