def main(): args = get_args() if args.debug: logger.setLevel(logging.DEBUG) if args.account: # ## Connect via Account account = MyPlexAccount(args.username, args.password) plex = account.resource(args.resource).connect() elif args.server: # ## Connect via Direct URL baseurl = args.baseurl token = args.token plex = PlexServer(baseurl, token) else: exit(1) all_shows = plex.library.section('TV Shows') # shows = get_unwatched_shows(all_shows.all()) episodes = get_random_episodes(all_shows, n=args.number) for episode in episodes: season_episode = episode.seasonEpisode # skipped = skipped_missing(all_shows.get(title=episode.grandparentTitle), episode) # playlist = Playlist(plex, ) plex.playlist(title=args.name).delete() Playlist.create(server=plex, title=args.name, items=episodes)
def sync_all(): global user_plex plex = PlexServer(PLEX_URL, PLEX_TOKEN) plex_users = get_user_tokens(plex.machineIdentifier) user_token = plex_users.get(FROM_USER) playlists_to_sync = get_playlists_from_user(user_token) for playlist in playlists_to_sync: playlist_items = playlist.items() for user in TO_USERS: user_token = plex_users.get(user) user_plex = PlexServer(PLEX_URL, user_token) # Delete the old playlist try: logging.info( f'Syncing Playlist {playlist.title} to user {user}') user_playlist = user_plex.playlist(playlist.title) user_playlist.delete() except Exception as e: logging.error(e) try: user_plex.createPlaylist(title=playlist.title, items=playlist_items) except Exception as e: logging.error(e)
def main(): """Main script""" plex = PlexServer(PLEX_URL, PLEX_TOKEN) plex_users = get_user_tokens(plex.machineIdentifier) plex_playlists = {playlist.title: playlist.items() for playlist in plex.playlists()} for playlist in PLAYLISTS: playlist_items = plex_playlists.get(playlist) if not playlist_items: print("Playlist '{playlist}' not found on the server. Skipping.".format(playlist=playlist)) continue print("Cloning the '{title}' playlist...".format(title=playlist)) for user in USERS: user_token = plex_users.get(user) if not user_token: print("...User '{user}' not found in shared users. Skipping.".format(user=user)) continue user_plex = PlexServer(PLEX_URL, user_token) # Delete the old playlist try: user_playlist = user_plex.playlist(playlist) user_playlist.delete() except: pass # Create a new playlist user_plex.createPlaylist(playlist, playlist_items) print("...Created playlist for '{user}'.".format(user=user)) return
def change_playlist_content(plex: PlexServer, name: str, tracks: List[Track]) -> Playlist: """ Connects to the PlexServer, and fills the named playlist with the given track list. If the playlist does not yet exist, it will be created. :param plex: the PlexServer object :param name: the name of the playlist to replace the contents of :param tracks: list of Tracks that will be the contents of the playlist :return: the Playlist object """ timer = Stopwatch() timer.start() logger = config.logger if not any([name == pl.title for pl in plex.playlists()]): playlist = plex.createPlaylist(name, tracks) logger.debug(f"Created new playlist {name} in {timer.click():.2f}s") else: playlist = plex.playlist(name) [playlist.removeItem(item) for item in playlist.items()] logger.debug(f"Emptied playlist {name} in {timer.click():.2f}s") playlist.addItems(tracks) logger.debug( f"Added {len(tracks)} track to the playlist {name} in {timer.click():.2f}s" ) return playlist
def createPlaylist(plex: PlexServer, sp: spotipy.Spotify, playlist: []): playlistName = playlist['owner']['display_name'] + " - " + playlist['name'] logging.info('Starting playlist %s' % playlistName) plexTracks = getPlexTracks(plex, getSpotifyTracks(sp, playlist)) if len(plexTracks) > 0: try: plexPlaylist = plex.playlist(playlistName) logging.info('Updating playlist %s' % playlistName) plexPlaylist.addItems(plexTracks) except: logging.info("Creating playlist %s" % playlistName) plex.createPlaylist(playlistName, plexTracks)
def main(): """Main script""" global PLAYLISTS plex_server = PlexServer(PLEX_URL, PLEX_TOKEN) plex_users = get_user_tokens(plex_server.machineIdentifier) plex_users[plex_server.myPlexUsername] = plex_server._token plex_user = PlexServer(PLEX_URL, plex_users[FROM_USER]) plex_playlists = { playlist.title: playlist.items() for playlist in plex_user.playlists() } if not PLAYLISTS: PLAYLISTS = plex_playlists # Default to all playlists for playlist in PLAYLISTS: if playlist in SKIP_PLAYLISTS: print("Skipping '{playlist}'...".format(playlist=playlist)) continue playlist_items = plex_playlists.get(playlist) if not playlist_items: print("Playlist '{playlist}' not found on the server. Skipping.". format(playlist=playlist)) continue print("Cloning the '{title}' playlist...".format(title=playlist)) for user in TO_USERS: user_token = plex_users.get(user) if not user_token: print("...User '{user}' not found in shared users. Skipping.". format(user=user)) continue user_plex = PlexServer(PLEX_URL, user_token) # Delete the old playlist try: user_playlist = user_plex.playlist(playlist) user_playlist.delete() except: pass # Create a new playlist user_plex.createPlaylist(playlist, playlist_items) print("...Created playlist for '{user}'.".format(user=user)) return
def main(): config = load_config() account = MyPlexAccount(config.get('DEFAULT', 'username'), config.get('DEFAULT', 'password')) admin_server = account.resource(config.get('DEFAULT', 'server name')).connect() user = account.user(config.get('DEFAULT', 'user')) # Get the token for the machine. token = user.get_token(admin_server.machineIdentifier) # Get the user server. We access the base URL by requesting a URL for a # blank key. user_server = PlexServer(admin_server.url(''), token=token) albums = {a.key: a for a in user_server.library.section('Music').albums()} playlist = user_server.playlist(config.get('DEFAULT', 'playlist name')) items = playlist.items() sort_structure = [] for track in items: album = albums[track.parentKey] key = get_track_sort_key(track, album) sort_structure.append((key, track)) sort_structure.sort(key=lambda item: item[0]) items = [item[1] for item in sort_structure] for item in items: playlist.removeItem(item) playlist.addItems(items)
class SpotiPlex: def __init__(self, spotifyInfo, plexInfo): self.credManager = SpotifyClientCredentials( client_id=spotifyInfo['clientId'], client_secret=spotifyInfo['clientSecret']) self.user = spotifyInfo['user'] self.sp = spotipy.Spotify(client_credentials_manager=self.credManager) self.plex = PlexServer(plexInfo['url'], plexInfo['token']) def getSpotifyPlaylist(self, plId): 'Generate and return a list of tracks of the Spotify playlist' #playlist = self.sp.user_playlist(self.user, plId) playlist = self.sp.user_playlist_tracks(self.user, playlist_id=plId) tracks = playlist['items'] while playlist['next']: playlist = self.sp.next(playlist) tracks.extend(playlist['items']) items = [] for item in tracks: items.append({ 'title': item['track']['name'], 'album': item['track']['album']['name'], 'artist': item['track']['artists'][0]['name'], 'isrc': item['track']['external_ids']['isrc'] #'number': item['track']['track_number'], #'img': item['track']['album']['images'][0]['url'] }) return items def checkPlexFiles(self, playlist): 'Check if the songs in the playlist are present on the Plex server. Returns list of found and missing items' tracks = [] missing = [] for item in playlist: results = self.plex.search(item['title'], mediatype='track') if not results: missing.append(item) continue for result in results: if type(result) != plexapi.audio.Track: continue else: if result.grandparentTitle.lower() == item['artist'].lower( ): # and result.parentTitle == item['album']: tracks.append(result) break else: if result == results[-1]: missing.append(item) break return tracks, missing def checkForPlexPlaylist(self, name): 'Check if a playlist with this name exists in Plex. Returns the playlist if valid, else None' try: return self.plex.playlist(name) except plexapi.exceptions.NotFound: return None def comparePlaylists(self, sPlaylist, pPlaylist): 'Compares the extracted Spotify playlist with the existing Plex playlist. Returns list of tracks to create the new playlist version from and missing songs in Plex' tracksToAdd = sPlaylist plexTracks = pPlaylist.items() plexOnlyItems = [] temp = [] for track in plexTracks: # remove any tracks from Spotify list that are already in Plex lastLen = len(temp) temp = list( filter(lambda item: not item['title'] == track.title, tracksToAdd)) if not len(temp) == lastLen: tracksToAdd = temp else: plexOnlyItems.append(track) return tracksToAdd, plexOnlyItems def createPlexPlaylist(self, name, playlist=None): 'Create the playlist on the Plex server from given name and a item list' newPlaylist = self.plex.createPlaylist(name, items=playlist) return def addToPlexPlaylist(self, plexPlaylist, newItems): 'Add more items to a Plex playlist' return plexPlaylist.addItems(newItems) def removeFromPlexPlaylist(self, plexPlaylist, itemsToRemove): 'Remove given items from a Plex playlist' ## Seems not to work properly yet for item in itemsToRemove: plexPlaylist.removeItem(item)
def main(): """Main script""" num_cores = multiprocessing.cpu_count() l = Library(FILEPATH) playlists = l.getPlaylistNames() PLEX = PlexServer(PLEX_URL, PLEX_TOKEN) PLEX_USERS = get_user_tokens(PLEX.machineIdentifier) PLEX_MUSIC = PLEX.library.section('Music') PLEX_TRACK_LIST = PLEX_MUSIC.searchTracks() PLEX_ARTIST_LIST = PLEX_MUSIC.searchArtists() for playlist in playlists: playlist_items = [] DATA_FILE = playlist + '.pickle' if playlist not in PLAYLISTS: continue # Check if .pickle exists try: print("Loading the '{title}' playlist from disk...".format(title=playlist)) with open(DATA_FILE, 'rb') as fp: playlist_items = pickle.load(fp) fp.close() # HACK playlist_items = [playlist_item for playlist_item in playlist_items if playlist_item] except FileNotFoundError: print("Building the '{title}' playlist...".format(title=playlist)) PLAYLIST_TRACKS = l.getPlaylist(playlist).tracks # Multiprocessing implementation # playlist_items = Parallel(n_jobs=num_cores, prefer='processes')( # delayed(match_track)(PLAYLIST_TRACK, PLEX_MUSIC, PLEX_ARTIST_LIST, PLEX_TRACK_LIST) for PLAYLIST_TRACK in PLAYLIST_TRACKS) # Standard implementation for PLAYLIST_TRACK in PLAYLIST_TRACKS: track_match = match_track(PLAYLIST_TRACK, PLEX_MUSIC, PLEX_ARTIST_LIST, PLEX_TRACK_LIST) if track_match: playlist_items.append(track_match) # Save data (just in case) with open(DATA_FILE, 'wb') as fp: pickle.dump(playlist_items, fp) fp.close() # Create playlist (per user) for user in USERS: user_token = PLEX_USERS.get(user) if not user_token: print("...User '{user}' not found in shared users. Skipping.".format(user=user)) continue user_plex = PlexServer(PLEX_URL, user_token) # Delete the old playlist try: user_playlist = user_plex.playlist(playlist) user_playlist.delete() except: pass # Create a new playlist user_plex.createPlaylist(playlist, playlist_items) print("...Created playlist for '{user}'.".format(user=user)) return
class tizplexproxy(object): """A class that accesses Plex servers, retrieves track URLs and creates and manages a playback queue. """ def __init__(self, base_url, token): self.base_url = base_url self.queue = list() self.queue_index = -1 self.play_queue_order = list() self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"]) self.current_play_mode = self.play_modes.NORMAL self.now_playing_track = None self._plex = PlexServer(base_url, token) self._music = self._plex.library.section('Music') def set_play_mode(self, mode): """ Set the playback mode. :param mode: current valid values are "NORMAL" and "SHUFFLE" """ self.current_play_mode = getattr(self.play_modes, mode) self.__update_play_queue_order() def enqueue_audio_tracks(self, arg): """Search the Plex server for audio tracks and add them to the playback queue. :param arg: a search string """ logging.info('arg : %s', arg) print_msg("[Plex] [Track search in server] : '{0}'. " \ .format(self.base_url)) try: count = len(self.queue) try: tracks = self._music.searchTracks(title=arg) for track in tracks: track_info = TrackInfo(track, track.artist(), track.album()) self.add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): tracks = self._music.search(libtype='track') for track in tracks: track_name = track.title if fuzz.partial_ratio(arg, track_name) > 60: track_info = TrackInfo(track, track.artist(), track.album()) self.add_to_playback_queue(track_info) if count == len(self.queue): raise ValueError self.__update_play_queue_order() except ValueError: raise ValueError(str("Track not found : %s" % arg)) def enqueue_audio_artist(self, arg): """Obtain an artist from the Plex server and add all the artist's audio tracks to the playback queue. :param arg: an artist search term """ logging.info('arg : %s', arg) print_msg("[Plex] [Artist search in server] : '{0}'. " \ .format(self.base_url)) try: count = len(self.queue) artist = None artist_name = '' try: artists = self._music.searchArtists(title=arg) for artist in artists: artist_name = artist.title print_wrn("[Plex] Playing '{0}'." \ .format(artist_name.encode('utf-8'))) for album in artist.albums(): for track in album.tracks(): track_info = TrackInfo(track, artist, album) self.add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): artist_dict = dict() artist_names = list() artists = self._music.search(libtype='artist') for art in artists: artist_names.append(art.title) artist_dict[art.title] = art if len(artist_names) > 1: artist_name = process.extractOne(arg, artist_names)[0] artist = artist_dict[artist_name] elif len(artist_names) == 1: artist_name = artist_names[0] artist = artist_dict[artist_name] if artist: print_wrn("[Plex] '{0}' not found. " \ "Playing '{1}' instead." \ .format(arg.encode('utf-8'), \ artist_name.encode('utf-8'))) for album in artist.albums(): for track in album.tracks(): track_info = TrackInfo(track, artist, album) self.add_to_playback_queue(track_info) if count == len(self.queue): raise ValueError self.__update_play_queue_order() except ValueError: raise ValueError(str("Artist not found : %s" % arg)) def enqueue_audio_album(self, arg): """Obtain an album from the Plex server and add all its tracks to the playback queue. :param arg: an album search term """ logging.info('arg : %s', arg) print_msg("[Plex] [Album search in server] : '{0}'. " \ .format(self.base_url)) try: count = len(self.queue) album = None album_name = '' try: albums = self._music.searchAlbums(title=arg) for album in albums: album_name = album.title print_wrn("[Plex] Playing '{0}'." \ .format(album_name.encode('utf-8'))) for track in album.tracks(): track_info = TrackInfo(track, track.artist(), album) self.add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): album_dict = dict() album_names = list() albums = self._music.search(libtype='album') for alb in albums: album_names.append(alb.title) album_dict[alb.title] = alb if len(album_names) > 1: album_name = process.extractOne(arg, album_names)[0] album = album_dict[album_name] elif len(album_names) == 1: album_name = album_names[0] album = album_dict[album_name] if album: print_wrn("[Plex] '{0}' not found. " \ "Playing '{1}' instead." \ .format(arg.encode('utf-8'), \ album_name.encode('utf-8'))) for track in album.tracks(): track_info = TrackInfo(track, album, album) self.add_to_playback_queue(track_info) if count == len(self.queue): raise ValueError self.__update_play_queue_order() except ValueError: raise ValueError(str("Album not found : %s" % arg)) def enqueue_audio_playlist(self, arg): """Add all audio tracks in a Plex playlist to the playback queue. :param arg: a playlist search term """ logging.info('arg : %s', arg) print_msg("[Plex] [Playlist search in server] : '{0}'. " \ .format(self.base_url)) try: count = len(self.queue) playlist_title = '' playlist = None try: playlist = self._plex.playlist(title=arg) if playlist: playlist_title = playlist.title print_wrn("[Plex] Playing '{0}'." \ .format(playlist_title.encode('utf-8'))) for item in playlist.items(): if item.TYPE == 'track': track = item track_info = TrackInfo(track, track.artist(), \ track.album()) self.add_to_playback_queue(track_info) if count == len(self.queue): print_wrn("[Plex] '{0}' No audio tracks found." \ .format(playlist_title.encode('utf-8'))) raise ValueError except (NotFound): pass if count == len(self.queue): playlist_dict = dict() playlist_titles = list() playlists = self._plex.playlists() for pl in playlists: playlist_titles.append(pl.title) playlist_dict[pl.title] = pl if len(playlist_titles) > 1: playlist_title = process.extractOne(arg, playlist_titles)[0] playlist = playlist_dict[playlist_title] elif len(playlist_titles) == 1: playlist_title = playlist_titles[0] playlist = playlist_dict[playlist_title] if playlist: print_wrn("[Plex] '{0}' not found. " \ "Playing '{1}' instead." \ .format(arg.encode('utf-8'), \ playlist_title.encode('utf-8'))) for item in playlist.items(): if item.TYPE == 'track': track = item track_info = TrackInfo(track, track.artist(), \ track.album()) self.add_to_playback_queue(track_info) if count == len(self.queue): print_wrn("[Plex] '{0}' No audio tracks found." \ .format(playlist_title.encode('utf-8'))) if count == len(self.queue): raise ValueError self.__update_play_queue_order() except (ValueError, NotFound): raise ValueError( str("Playlist not found or no audio tracks in playlist : %s" % arg)) def current_audio_track_title(self): """ Retrieve the current track's title. """ track = self.now_playing_track title = '' if track: title = to_ascii(track.title).encode("utf-8") return title def current_audio_track_artist(self): """ Retrieve the current track's artist. """ track = self.now_playing_track artist = '' if track: artist = to_ascii(track.artist).encode("utf-8") return artist def current_audio_track_album(self): """ Retrieve the current track's album. """ track = self.now_playing_track album = '' if track: album = to_ascii(track.album).encode("utf-8") return album def current_audio_track_year(self): """ Retrieve the current track's publication year. """ track = self.now_playing_track year = 0 if track: year = track.year return year def current_audio_track_file_size(self): """ Retrieve the current track's file size. """ track = self.now_playing_track size = 0 if track: size = track.size return size def current_audio_track_duration(self): """ Retrieve the current track's duration. """ track = self.now_playing_track duration = 0 if track: duration = track.duration return duration def current_audio_track_bitrate(self): """ Retrieve the current track's bitrate. """ track = self.now_playing_track bitrate = 0 if track: bitrate = track.bitrate return bitrate def current_audio_track_codec(self): """ Retrieve the current track's codec. """ track = self.now_playing_track codec = '' if track: codec = to_ascii(track.codec).encode("utf-8") return codec def current_audio_track_album_art(self): """ Retrieve the current track's album_art. """ track = self.now_playing_track album_art = '' if track: album_art = to_ascii(track.thumb_url).encode("utf-8") return album_art def current_audio_track_queue_index_and_queue_length(self): """ Retrieve index in the queue (starting from 1) of the current track and the length of the playback queue. """ return self.play_queue_order[self.queue_index] + 1, len(self.queue) def clear_queue(self): """ Clears the playback queue. """ self.queue = list() self.queue_index = -1 def remove_current_url(self): """Remove the currently active url from the playback queue. """ logging.info("") if len(self.queue) and self.queue_index: track = self.queue[self.queue_index] print_nfo("[Plex] [Track] '{0}' removed." \ .format(to_ascii(track['i'].title).encode("utf-8"))) del self.queue[self.queue_index] self.queue_index -= 1 if self.queue_index < 0: self.queue_index = 0 self.__update_play_queue_order() def next_url(self): """ Retrieve the url of the next track in the playback queue. """ logging.info("") try: if len(self.queue): self.queue_index += 1 if (self.queue_index < len(self.queue)) \ and (self.queue_index >= 0): next_track = self.queue[self.play_queue_order \ [self.queue_index]] return self.__retrieve_track_url(next_track) else: self.queue_index = -1 return self.next_url() else: return '' except (KeyError, AttributeError): # TODO: We don't remove this for now # del self.queue[self.queue_index] logging.info("exception") return self.next_url() def prev_url(self): """ Retrieve the url of the previous track in the playback queue. """ logging.info("") try: if len(self.queue): self.queue_index -= 1 if (self.queue_index < len(self.queue)) \ and (self.queue_index >= 0): prev_track = self.queue[self.play_queue_order \ [self.queue_index]] return self.__retrieve_track_url(prev_track) else: self.queue_index = len(self.queue) return self.prev_url() else: return '' except (KeyError, AttributeError): # TODO: We don't remove this for now # del self.queue[self.queue_index] logging.info("exception") return self.prev_url() def __update_play_queue_order(self): """ Update the queue playback order. A sequential order is applied if the current play mode is "NORMAL" or a random order if current play mode is "SHUFFLE" """ total_tracks = len(self.queue) if total_tracks: if not len(self.play_queue_order): # Create a sequential play order, if empty self.play_queue_order = range(total_tracks) if self.current_play_mode == self.play_modes.SHUFFLE: random.shuffle(self.play_queue_order) print_nfo("[Plex] [Tracks in queue] '{0}'." \ .format(total_tracks)) def __retrieve_track_url(self, track): """ Retrieve a track url """ try: self.now_playing_track = track return track.url.encode("utf-8") except AttributeError: logging.info("Could not retrieve the track url!") raise def add_to_playback_queue(self, track): """ Add to the playback queue. """ print_nfo("[Plex] [Track] '{0}' [{1}]." \ .format(to_ascii(track.title).encode("utf-8"), \ to_ascii(track.codec))) queue_index = len(self.queue) self.queue.append(track)
ep_list = [] to_remove = '' if opts.user: user_acct = account.user(opts.user) plex_server = PlexServer(PLEX_URL, user_acct.get_token(plex.machineIdentifier)) else: plex_server = plex if opts.shows and not opts.playlist: to_remove = opts.shows elif not opts.shows and opts.playlist: to_remove = [ x.grandparentTitle for x in plex_server.playlist(opts.playlist).items() ] if opts.action == 'deck': if not to_remove: print('The following shows are On Deck...') on_deck = get_on_deck(plex_server)['on_deck'] for item in on_deck: print('{}: S{:02}E{:02} {}'.format( item.grandparentTitle.encode('UTF-8'), int(item.parentIndex), int(item.index), item.title.encode('UTF-8'))) else: print('Finding listed shows On Deck...') while True:
class Plex(commands.Cog): """ Discord commands pertinent to interacting with Plex Contains user commands such as play, pause, resume, stop, etc. Grabs, and parses all data from plex database. """ # pylint: disable=too-many-instance-attributes # All are necessary to detect global interactions # within the bot. def __init__(self, bot, **kwargs): """ Initializes Plex resources Connects to Plex library and sets up all asyncronous communications. Args: bot: discord.ext.command.Bot, bind for cogs base_url: str url to Plex server plex_token: str X-Token of Plex server lib_name: str name of Plex library to search through Raises: plexapi.exceptions.Unauthorized: Invalid Plex token Returns: None """ self.bot = bot self.base_url = kwargs["base_url"] self.plex_token = kwargs["plex_token"] self.library_name = kwargs["lib_name"] self.bot_prefix = bot.command_prefix if kwargs["lyrics_token"]: self.genius = lyricsgenius.Genius(kwargs["lyrics_token"]) else: plex_log.warning("No lyrics token specified, lyrics disabled") self.genius = None # Log fatal invalid plex token try: self.pms = PlexServer(self.base_url, self.plex_token) except Unauthorized: plex_log.fatal("Invalid Plex token, stopping...") raise Unauthorized("Invalid Plex token") self.music = self.pms.library.section(self.library_name) plex_log.debug("Connected to plex library: %s", self.library_name) # Initialize necessary vars self.voice_channel = None self.current_track = None self.np_message_id = None self.ctx = None # Initialize events self.play_queue = asyncio.Queue() self.play_next_event = asyncio.Event() bot_log.info("Started bot successfully") self.bot.loop.create_task(self._audio_player_task()) def _search_tracks(self, title: str): """ Search the Plex music db for track Args: title: str title of song to search for Returns: plexapi.audio.Track pointing to best matching title Raises: MediaNotFoundError: Title of track can't be found in plex db """ results = self.music.searchTracks(title=title, maxresults=1) try: return results[0] except IndexError: raise MediaNotFoundError("Track cannot be found") def _search_albums(self, title: str): """ Search the Plex music db for album Args: title: str title of album to search for Returns: plexapi.audio.Album pointing to best matching title Raises: MediaNotFoundError: Title of album can't be found in plex db """ results = self.music.searchAlbums(title=title, maxresults=1) try: return results[0] except IndexError: raise MediaNotFoundError("Album cannot be found") def _search_playlists(self, title: str): """ Search the Plex music db for playlist Args: title: str title of playlist to search for Returns: plexapi.playlist pointing to best matching title Raises: MediaNotFoundError: Title of playlist can't be found in plex db """ try: return self.pms.playlist(title) except NotFound: raise MediaNotFoundError("Playlist cannot be found") async def _play(self): """ Heavy lifting of playing songs Grabs the appropiate streaming URL, sends the `now playing` message, and initiates playback in the vc. Args: None Returns: None Raises: None """ track_url = self.current_track.getStreamURL() audio_stream = FFmpegPCMAudio(track_url) while self.voice_channel.is_playing(): asyncio.sleep(2) self.voice_channel.play(audio_stream, after=self._toggle_next) plex_log.debug("%s - URL: %s", self.current_track, track_url) embed, img = self._build_embed_track(self.current_track) self.np_message_id = await self.ctx.send(embed=embed, file=img) async def _audio_player_task(self): """ Coroutine to handle playback and queuing Always-running function awaiting new songs to be added. Auto disconnects from VC if idle for > 15 seconds. Handles auto deletion of now playing song notifications. Args: None Returns: None Raises: None """ while True: self.play_next_event.clear() if self.voice_channel: try: # Disconnect after 15 seconds idle async with timeout(15): self.current_track = await self.play_queue.get() except asyncio.TimeoutError: await self.voice_channel.disconnect() self.voice_channel = None if not self.current_track: self.current_track = await self.play_queue.get() await self._play() await self.play_next_event.wait() await self.np_message_id.delete() def _toggle_next(self, error=None): """ Callback for vc playback Clears current track, then activates _audio_player_task to play next in queue or disconnect. Args: error: Optional parameter required for discord.py callback Returns: None Raises: None """ self.current_track = None self.bot.loop.call_soon_threadsafe(self.play_next_event.set) @staticmethod def _build_embed_track(track, type_="play"): """ Creates a pretty embed card for tracks Builds a helpful status embed with the following info: Status, song title, album, artist and album art. All pertitent information is grabbed dynamically from the Plex db. Args: track: plexapi.audio.Track object of song type_: Type of card to make (play, queue). Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of album thumbnail img. Raises: ValueError: Unsupported type of embed {type_} """ # Grab the relevant thumbnail img_stream = requests.get(track.thumbUrl, stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") # Get appropiate status message if type_ == "play": title = f"Now Playing - {track.title}" elif type_ == "queue": title = f"Added to queue - {track.title}" else: raise ValueError(f"Unsupported type of embed {type_}") # Include song details descrip = f"{track.album().title} - {track.artist().title}" # Build the actual embed embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") # Point to file attached with ctx object. embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for track - %s", track.title) return embed, art_file @staticmethod def _build_embed_album(album): """ Creates a pretty embed card for albums Builds a helpful status embed with the following info: album, artist, and album art. All pertitent information is grabbed dynamically from the Plex db. Args: album: plexapi.audio.Album object of album Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of album thumbnail img. Raises: None """ # Grab the relevant thumbnail img_stream = requests.get(album.thumbUrl, stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") title = "Added album to queue" descrip = f"{album.title} - {album.artist().title}" embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for album - %s", album.title) return embed, art_file @staticmethod def _build_embed_playlist(self, playlist): """ Creates a pretty embed card for playlists Builds a helpful status embed with the following info: playlist art. All pertitent information is grabbed dynamically from the Plex db. Args: playlist: plexapi.playlist object of playlist Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of playlist thumbnail img. Raises: None """ # Grab the relevant thumbnail img_stream = requests.get(self.pms.url(playlist.composite, True), stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") title = "Added playlist to queue" descrip = f"{playlist.title}" embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for playlist - %s", playlist.title) return embed, art_file async def _validate(self, ctx): """ Ensures user is in a vc Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: VoiceChannelError: Author not in voice channel """ # Fail if user not in vc if not ctx.author.voice: await ctx.send("Join a voice channel first!") bot_log.debug("Failed to play, requester not in voice channel") raise VoiceChannelError # Connect to voice if not already if not self.voice_channel: self.voice_channel = await ctx.author.voice.channel.connect() bot_log.debug("Connected to vc.") @command() async def play(self, ctx, *args): """ User command to play song Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of song to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: track = self._search_tracks(title) except MediaNotFoundError: await ctx.send(f"Can't find song: {title}") bot_log.debug("Failed to play, can't find song - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass # Specific add to queue message if self.voice_channel.is_playing(): bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_track(track, type_="queue") await ctx.send(embed=embed, file=img) # Add the song to the async queue await self.play_queue.put(track) @command() async def album(self, ctx, *args): """ User command to play song Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of song to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: album = self._search_albums(title) except MediaNotFoundError: await ctx.send(f"Can't find album: {title}") bot_log.debug("Failed to queue album, can't find - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_album(album) await ctx.send(embed=embed, file=img) for track in album.tracks(): await self.play_queue.put(track) @command() async def playlist(self, ctx, *args): """ User command to play playlist Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of playlist to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: playlist = self._search_playlists(title) except MediaNotFoundError: await ctx.send(f"Can't find playlist: {title}") bot_log.debug("Failed to queue playlist, can't find - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_playlist(self, playlist) await ctx.send(embed=embed, file=img) for item in playlist.items(): if (item.TYPE == "track"): await self.play_queue.put(item) @command() async def stop(self, ctx): """ User command to stop playback Stops playback and disconnects from vc. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.stop() await self.voice_channel.disconnect() self.voice_channel = None self.ctx = None bot_log.debug("Stopped") await ctx.send(":stop_button: Stopped") @command() async def pause(self, ctx): """ User command to pause playback Pauses playback, but doesn't reset anything to allow playback resuming. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.pause() bot_log.debug("Paused") await ctx.send(":play_pause: Paused") @command() async def resume(self, ctx): """ User command to resume playback Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.resume() bot_log.debug("Resumed") await ctx.send(":play_pause: Resumed") @command() async def skip(self, ctx): """ User command to skip song in queue Skips currently playing song. If no other songs in queue, stops playback, otherwise moves to next song. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ bot_log.debug("Skip") if self.voice_channel: self.voice_channel.stop() bot_log.debug("Skipped") self._toggle_next() @command(name="np") async def now_playing(self, ctx): """ User command to get currently playing song. Deletes old `now playing` status message, Creates a new one with up to date information. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.current_track: embed, img = self._build_embed_track(self.current_track) bot_log.debug("Now playing") if self.np_message_id: await self.np_message_id.delete() bot_log.debug("Deleted old np status") bot_log.debug("Created np status") self.np_message_id = await ctx.send(embed=embed, file=img) @command() async def clear(self, ctx): """ User command to clear play queue. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ self.play_queue = asyncio.Queue() bot_log.debug("Cleared queue") await ctx.send(":boom: Queue cleared.") @command() async def lyrics(self, ctx): """ User command to get lyrics of a song. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if not self.current_track: plex_log.info("No song currently playing") return if self.genius: plex_log.info( "Searching for %s, %s", self.current_track.title, self.current_track.artist().title, ) try: song = self.genius.search_song( self.current_track.title, self.current_track.artist().title ) except TypeError: self.genius = None plex_log.error("Invalid genius token, disabling lyrics") return try: lyrics = song.lyrics # Split into 1950 char chunks # Discord max message length is 2000 lines = [(lyrics[i : i + 1950]) for i in range(0, len(lyrics), 1950)] for i in lines: if i == "": continue # Apply code block format i = f"```{i}```" await ctx.send(i) except (IndexError, TypeError): plex_log.info("Could not find lyrics") await ctx.send("Can't find lyrics for this song.") else: plex_log.warning("Attempted lyrics without valid token")
class tizplexproxy(object): """A class that accesses Plex servers, retrieves track URLs and creates and manages a playback queue. """ def __init__(self, base_url, token, section): self.base_url = base_url self.queue = list() self.queue_index = -1 self.play_queue_order = list() self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"]) self.current_play_mode = self.play_modes.NORMAL self.now_playing_track = None self._plex = PlexServer(base_url, token) self._music = self._plex.library.section(section) def set_play_mode(self, mode): """ Set the playback mode. :param mode: current valid values are "NORMAL" and "SHUFFLE" """ self.current_play_mode = getattr(self.play_modes, mode) self._update_play_queue_order() def enqueue_audio_tracks(self, arg): """Search the Plex server for audio tracks and add them to the playback queue. :param arg: a search string """ logging.info("arg : %s", arg) print_msg("[Plex] [Track search in server] : '{0}'. ".format( self.base_url)) try: count = len(self.queue) try: tracks = self._music.searchTracks(title=arg) for track in tracks: track_info = TrackInfo(track, track.artist(), track.album()) self._add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): tracks = self._music.search(libtype="track") for track in tracks: track_name = track.title if fuzz.partial_ratio(arg, track_name) > 60: track_info = TrackInfo(track, track.artist(), track.album()) self._add_to_playback_queue(track_info) self._finalise_play_queue(count, arg) except ValueError: raise ValueError(str("Track not found : %s" % arg)) def enqueue_audio_artist(self, arg): """Obtain an artist from the Plex server and add all the artist's audio tracks to the playback queue. :param arg: an artist search term """ logging.info("arg : %s", arg) print_msg("[Plex] [Artist search in server] : '{0}'. ".format( self.base_url)) try: count = len(self.queue) artist = None artist_name = "" try: artists = self._music.searchArtists(title=arg) for artist in artists: artist_name = artist.title print_wrn("[Plex] Playing '{0}'.".format(artist_name)) for album in artist.albums(): for track in album.tracks(): track_info = TrackInfo(track, artist, album) self._add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): artist_dict = dict() artist_names = list() artists = self._music.search(libtype="artist") for art in artists: artist_names.append(art.title) artist_dict[art.title] = art if len(artist_names) > 1: artist_name = process.extractOne(arg, artist_names)[0] artist = artist_dict[artist_name] elif len(artist_names) == 1: artist_name = artist_names[0] artist = artist_dict[artist_name] if artist: print_adv("[Plex] '{0}' not found. " "Playing '{1}' instead.".format( arg, artist_name)) for album in artist.albums(): for track in album.tracks(): track_info = TrackInfo(track, artist, album) self._add_to_playback_queue(track_info) self._finalise_play_queue(count, arg) except ValueError: raise ValueError(str("Artist not found : %s" % arg)) def enqueue_audio_album(self, arg): """Obtain an album from the Plex server and add all its tracks to the playback queue. :param arg: an album search term """ logging.info("arg : %s", arg) print_msg("[Plex] [Album search in server] : '{0}'. ".format( self.base_url)) try: count = len(self.queue) album = None album_name = "" try: albums = self._music.searchAlbums(title=arg) for album in albums: album_name = album.title print_wrn("[Plex] Playing '{0}'.".format(album_name)) for track in album.tracks(): track_info = TrackInfo(track, track.artist(), album) self._add_to_playback_queue(track_info) except (NotFound): pass if count == len(self.queue): album_dict = dict() album_names = list() albums = self._music.search(libtype="album") for alb in albums: album_names.append(alb.title) album_dict[alb.title] = alb if len(album_names) > 1: album_name = process.extractOne(arg, album_names)[0] album = album_dict[album_name] elif len(album_names) == 1: album_name = album_names[0] album = album_dict[album_name] if album: print_adv("[Plex] '{0}' not found. " "Playing '{1}' instead.".format(arg, album_name)) for track in album.tracks(): track_info = TrackInfo(track, album, album) self._add_to_playback_queue(track_info) self._finalise_play_queue(count, arg) except ValueError: raise ValueError(str("Album not found : %s" % arg)) def enqueue_audio_playlist(self, arg): """Add all audio tracks in a Plex playlist to the playback queue. :param arg: a playlist search term """ logging.info("arg : %s", arg) print_msg("[Plex] [Playlist search in server] : '{0}'. ".format( self.base_url)) try: count = len(self.queue) playlist_title = "" playlist = None try: playlist = self._plex.playlist(title=arg) if playlist: playlist_title = playlist.title print_wrn("[Plex] Playing '{0}'.".format(playlist_title)) for item in list(playlist.items()): if item.TYPE == "track": track = item track_info = TrackInfo(track, track.artist(), track.album()) self._add_to_playback_queue(track_info) if count == len(self.queue): print_wrn( "[Plex] '{0}' No audio tracks found.".format( playlist_title)) raise ValueError except (NotFound): pass if count == len(self.queue): playlist_dict = dict() playlist_titles = list() playlists = self._plex.playlists() for pl in playlists: playlist_titles.append(pl.title) playlist_dict[pl.title] = pl if len(playlist_titles) > 1: playlist_title = process.extractOne(arg, playlist_titles)[0] playlist = playlist_dict[playlist_title] elif len(playlist_titles) == 1: playlist_title = playlist_titles[0] playlist = playlist_dict[playlist_title] if playlist: print_adv("[Plex] '{0}' not found. " "Playing '{1}' instead.".format( arg, playlist_title)) for item in list(playlist.items()): if item.TYPE == "track": track = item track_info = TrackInfo(track, track.artist(), track.album()) self._add_to_playback_queue(track_info) if count == len(self.queue): print_wrn( "[Plex] '{0}' No audio tracks found.".format( playlist_title)) self._finalise_play_queue(count, arg) except (ValueError, NotFound): raise ValueError( str("Playlist not found or no audio tracks in playlist : %s" % arg)) def current_audio_track_title(self): """ Retrieve the current track's title. """ logging.info("current_audio_track_title") track = self.now_playing_track title = "" if track: title = to_ascii(track.title) return title def current_audio_track_artist(self): """ Retrieve the current track's artist. """ logging.info("current_audio_track_artist") track = self.now_playing_track artist = "" if track: artist = to_ascii(track.artist) return artist def current_audio_track_album(self): """ Retrieve the current track's album. """ logging.info("current_audio_track_album") track = self.now_playing_track album = "" if track and track.album: album = to_ascii(track.album) return album def current_audio_track_year(self): """ Retrieve the current track's publication year. """ logging.info("current_audio_track_year") track = self.now_playing_track year = 0 if track: year = track.year return year def current_audio_track_file_size(self): """ Retrieve the current track's file size. """ logging.info("current_audio_track_file_size") track = self.now_playing_track size = 0 if track: size = track.size return size def current_audio_track_duration(self): """ Retrieve the current track's duration. """ logging.info("current_audio_track_duration") track = self.now_playing_track duration = 0 if track: duration = track.duration return duration def current_audio_track_bitrate(self): """ Retrieve the current track's bitrate. """ logging.info("current_audio_track_bitrate") track = self.now_playing_track bitrate = 0 if track: bitrate = track.bitrate return bitrate def current_audio_track_codec(self): """ Retrieve the current track's codec. """ logging.info("current_audio_track_codec") track = self.now_playing_track codec = "" if track: codec = to_ascii(track.codec) return codec def current_audio_track_album_art(self): """ Retrieve the current track's album_art. """ logging.info("current_audio_track_album_art") track = self.now_playing_track album_art = "" if track and track.thumb_url: album_art = to_ascii(track.thumb_url) return album_art def current_audio_track_queue_index_and_queue_length(self): """ Retrieve index in the queue (starting from 1) of the current track and the length of the playback queue. """ logging.info("current_audio_track_queue_index_and_queue_length") return self.play_queue_order[self.queue_index] + 1, len(self.queue) def clear_queue(self): """ Clears the playback queue. """ self.queue = list() self.queue_index = -1 def print_queue(self): """ Print the contents of the playback queue. """ for i in range(0, len(self.queue)): track = self.queue[self.play_queue_order[i]] order_num = str("#{:0{}d}".format(i + 1, len(str(len(self.queue))))) info_str = str("[Plex] [Track] [{0}] '{1}' [{2}] ({3})".format( order_num, to_ascii(track.title), to_ascii(track.artist), to_ascii(track.duration_str), )) print_nfo(info_str + ".") print_nfo("[Plex] [Tracks in queue] '{0}'.".format(len(self.queue))) def remove_current_url(self): """Remove the currently active url from the playback queue. """ logging.info("") if len(self.queue) and self.queue_index: track = self.queue[self.queue_index] print_nfo("[Plex] [Track] '{0}' removed.".format( to_ascii(track["i"].title))) del self.queue[self.queue_index] self.queue_index -= 1 if self.queue_index < 0: self.queue_index = 0 self._update_play_queue_order() def next_url(self): """ Retrieve the url of the next track in the playback queue. """ logging.info("") try: if len(self.queue): self.queue_index += 1 if (self.queue_index < len(self.queue)) and (self.queue_index >= 0): next_track = self.queue[self.play_queue_order[ self.queue_index]] return self._retrieve_track_url(next_track) else: self.queue_index = -1 return self.next_url() else: return "" except (KeyError, AttributeError): # TODO: We don't remove this for now # del self.queue[self.queue_index] logging.info("exception") return self.next_url() def prev_url(self): """ Retrieve the url of the previous track in the playback queue. """ logging.info("") try: if len(self.queue): self.queue_index -= 1 if (self.queue_index < len(self.queue)) and (self.queue_index >= 0): prev_track = self.queue[self.play_queue_order[ self.queue_index]] return self._retrieve_track_url(prev_track) else: self.queue_index = len(self.queue) return self.prev_url() else: return "" except (KeyError, AttributeError): # TODO: We don't remove this for now # del self.queue[self.queue_index] logging.info("exception") return self.prev_url() def get_url(self, position=None): """Retrieve the url on a particular position in the playback queue. If no position is given, the url at the current position of the playback is returned. """ logging.info("get_url {}".format(position if position else "-1")) try: if len(self.queue): queue_pos = self.play_queue_order[self.queue_index] if position and position > 0 and position <= len(self.queue): self.queue_index = position - 1 queue_pos = self.play_queue_order[self.queue_index] logging.info("get_url : self.queue_index {}".format( self.queue_index)) logging.info("get_url : play_queue_order {}".format( self.play_queue_order[self.queue_index])) track = self.queue[queue_pos] return self._retrieve_track_url(track) else: return "" except (KeyError, AttributeError): # TODO: We don't remove this for now # del self.queue[self.queue_index] logging.info("exception") return "" def _update_play_queue_order(self): """ Update the queue playback order. A sequential order is applied if the current play mode is "NORMAL" or a random order if current play mode is "SHUFFLE" """ total_tracks = len(self.queue) if total_tracks: if not len(self.play_queue_order): # Create a sequential play order, if empty self.play_queue_order = list(range(total_tracks)) if self.current_play_mode == self.play_modes.SHUFFLE: random.shuffle(self.play_queue_order) def _retrieve_track_url(self, track): """ Retrieve a track url """ try: self.now_playing_track = track return track.url except AttributeError: logging.info("Could not retrieve the track url!") raise def _add_to_playback_queue(self, track): """ Add to the playback queue. """ if not track: return self.queue.append(track) def _finalise_play_queue(self, count, arg): """ Helper function to grou the various actions needed to ready play queue. """ if count == len(self.queue): logging.info("no tracks found arg : %s", arg) raise ValueError self._update_play_queue_order() self.print_queue()
opts = parser.parse_args() ep_list = [] to_remove = '' if opts.user: user_acct = account.user(opts.user) plex_server = PlexServer(PLEX_URL, user_acct.get_token(plex.machineIdentifier)) else: plex_server = plex if opts.shows and not opts.playlist: to_remove = opts.shows elif not opts.shows and opts.playlist: to_remove = [x.grandparentTitle for x in plex_server.playlist(opts.playlist).items()] if opts.action == 'deck': if not to_remove: print('The following shows are On Deck...') on_deck = get_on_deck(plex_server)['on_deck'] for item in on_deck: print('{}: S{:02}E{:02} {}'.format( item.grandparentTitle.encode('UTF-8'), int(item.parentIndex), int(item.index), item.title.encode('UTF-8'))) else: print('Finding listed shows On Deck...') while True: off_deck = get_on_deck(plex_server, to_remove)
print('finished refresing') notifier.stop() server = PlexServer(url, token) library = server.library section = library.section(section_title) notifier = server.startAlertListener(callback=cb) if section.refreshing: print('already refrehsing') else: print('start refrehsing') section.update() while notifier.isAlive(): sleep(0.1) print('addig item to playlist') items = section.searchTracks(title=item_title, sort='addedAt:desc', maxresults=1) print('items', items) if items: playlist = server.playlist(playlist_title) print('playlist', playlist) playlist.addItems(items) print('item added to playlist') else: print('no items found')