class Plex(GObject.Object): __gsignals__ = { 'login-status': (GObject.SignalFlags.RUN_FIRST, None, (bool, str)), 'shows-latest': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'shows-deck': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'section-shows-deck': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'download-cover': (GObject.SignalFlags.RUN_FIRST, None, (int, str)), 'download-from-url': (GObject.SignalFlags.RUN_FIRST, None, (str, str)), 'shows-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'item-downloading': (GObject.SignalFlags.RUN_FIRST, None, (object, bool)), 'sync-status': (GObject.SignalFlags.RUN_FIRST, None, (bool, )), 'servers-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'sections-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'album-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'artist-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'playlists-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'section-item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'search-item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (str, object)), 'connection-to-server': (GObject.SignalFlags.RUN_FIRST, None, ()), 'logout': (GObject.SignalFlags.RUN_FIRST, None, ()), 'loading': (GObject.SignalFlags.RUN_FIRST, None, (str, bool)), 'sync-items': (GObject.SignalFlags.RUN_FIRST, None, (object, )), } _config = {} _search_provider_data = {} _server = None _account = None _library = None _sync_busy = False def __init__(self, config_dir, data_dir, player, **kwargs): super().__init__(**kwargs) self._settings = Gio.Settings("nl.g4d.Girens") self._config_dir = config_dir self._data_dir = data_dir self._player = player self._player.set_plex(self) self._config = self.__open_file(self._config_dir + '/config') self._search_provider_data = self.__open_file(self._config_dir + '/search_provider_data') self._user_uuid = self._settings.get_string("user-uuid") self._token = self.get_token(self._user_uuid) self._server_uuid = self._settings.get_string("server-uuid") if (self._server_uuid is not ''): self._server_token = self.get_server_token(self._server_uuid) self._server_url = self._settings.get_string("server-url") else: self._server_token = None self._server_url = None def __open_file(self, file_path): if (os.path.isfile(file_path)): with open(file_path, 'r') as file: lines = file.readlines() return json.loads(lines[0]) return {} def has_token(self): return self._token is not None def get_server_token(self, uuid): return Secret.password_lookup_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': uuid}, None) def get_token(self, uuid): return Secret.password_lookup_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': uuid}, None) def set_server_token(self, token, server_url, server_uuid, name): self._settings.set_string("server-url", self._server._baseurl) self._settings.set_string("server-uuid", self._server.machineIdentifier) Secret.password_store( Secret.Schema.new( "nl.g4d.Girens", Secret.SchemaFlags.NONE, { 'name': Secret.SchemaAttributeType.STRING, 'url': Secret.SchemaAttributeType.STRING, 'uuid': Secret.SchemaAttributeType.STRING }), { 'name': name, 'url': server_url, 'uuid': server_uuid }, Secret.COLLECTION_DEFAULT, 'Girens server token', token, None, None) def set_token(self, token, username, email, uuid): self._settings.set_string("user-uuid", uuid) Secret.password_store( Secret.Schema.new( "nl.g4d.Girens", Secret.SchemaFlags.NONE, { 'username': Secret.SchemaAttributeType.STRING, 'email': Secret.SchemaAttributeType.STRING, 'uuid': Secret.SchemaAttributeType.STRING }), { 'username': username, 'email': email, 'uuid': uuid }, Secret.COLLECTION_DEFAULT, 'Girens token', token, None, None) def has_url(self): return self._server_url is not None def login_token(self, token): try: self._account = MyPlexAccount(token=token) self.set_token(self._account._token, self._account.username, self._account.email, self._account.uuid) self.emit('login-status', True, '') except: self.emit('login-status', False, 'Login failed') def login(self, username, password): try: self._account = MyPlexAccount(username, password) self.set_token(self._account._token, self._account.username, self._account.email, self._account.uuid) self.emit('login-status', True, '') except: self.emit('login-status', False, 'Login failed') def login_with_url(self, baseurl, token): try: self.emit('loading', _('Connecting to ') + baseurl, True) self._server = PlexServer(baseurl, token) self._account = self._server.account() self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._user_uuid}, None) self._user_uuid = None self._token = None self.emit('connection-to-server') self.emit('loading', 'Success', False) self.emit('login-status', True, '') except: self.emit('loading', _('Connecting to ') + baseurl + _(' failed.'), True) self.emit('login-status', False, 'Login failed') print('connection failed (login with url)') def __save_config(self): with open(self._config_dir + '/config', 'w') as file: file.write(json.dumps(self._config)) def __save_search_provider_data(self): with open(self._config_dir + '/search_provider_data', 'w') as file: file.write(json.dumps(self._search_provider_data)) def logout(self): self._config = {} self._server = None self._account = None self._library = None self.__remove_login() self.emit('logout') def __remove_login(self): if (os.path.isfile(self._config_dir + '/config')): os.remove(self._config_dir + '/config') Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._server_uuid}, None) Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._user_uuid}, None) self._settings.set_string("server-url", '') self._settings.set_string("server-uuid", '') self._settings.set_string("user-uuid", '') self._user_uuid = None self._token = None self._server_uuid = None self._server_token = None self._server_url = None def get_latest(self): latest = self._library.recentlyAdded() self.emit('shows-latest', latest) def get_deck(self): deck = self._library.onDeck() self.emit('shows-deck', deck) def get_section_deck(self, section_id): deck = self._library.sectionByID(section_id).onDeck() self.emit('section-shows-deck', deck) def get_item(self, key): return self._server.fetchItem(int(key)) def get_show(self, key): show = self._server.fetchItem(int(key)) episodes = show.episodes() self.emit('shows-retrieved', show, episodes) def get_album(self, key): album = self._server.fetchItem(int(key)) tracks = album.tracks() self.emit('album-retrieved', album, tracks) def get_artist(self, key): artist = self._server.fetchItem(int(key)) albums = artist.albums() self.emit('artist-retrieved', artist, albums) def get_servers(self): servers = [] if (self.has_token()): for resource in self._account.resources(): if (resource.provides == 'server'): servers.append(resource) else: servers.append(self._server) self.emit('servers-retrieved', servers) def get_playlists(self): playlists = self._server.playlists() self.emit('playlists-retrieved', playlists) def get_sections(self): sections = self._library.sections() self.emit('sections-retrieved', sections) def get_section_filter(self, section): if ('sections' in self._config and section.uuid in self._config['sections'] and 'sort' in self._config['sections'][section.uuid]): return self._config['sections'][section.uuid] return None def get_section_items(self, section, container_start=0, container_size=10, sort=None, sort_value=None): if (sort != None): if 'sections' not in self._config: self._config['sections'] = {} if section.uuid not in self._config['sections']: self._config['sections'][section.uuid] = {} self._config['sections'][section.uuid]['sort'] = sort self._config['sections'][section.uuid]['sort_value'] = sort_value self.__save_config() sort = sort + ':' + sort_value items = section.all(container_start=container_start, container_size=container_size, sort=sort) self.emit('section-item-retrieved', items) def reload_search_provider_data(self): #section = self._library.sectionByID('22') self._search_provider_data['sections'] = {} for section in self._library.sections(): if (section.type not in ['photo', 'movie']): items = section.all() self._search_provider_data['sections'][section.uuid] = { 'key': section.key, 'server_machine_identifier': self._server.machineIdentifier, 'title': section.title } self._search_provider_data['sections'][ section.uuid]['items'] = [] for item in items: self._search_provider_data['sections'][ section.uuid]['items'].append({ 'title': item.title, 'titleSort': item.titleSort, 'ratingKey': item.ratingKey, 'type': item.type }) if (section.type == 'artist'): for item in section.albums(): self._search_provider_data['sections'][ section.uuid]['items'].append({ 'title': item.title, 'titleSort': item.titleSort, 'ratingKey': item.ratingKey, 'type': item.type }) self.__save_search_provider_data() def search_library(self, search, libtype=None): items = self._library.search(search, limit=10, libtype=libtype) self.emit('search-item-retrieved', search, items) def download_cover(self, key, thumb): url_image = self._server.transcodeImage(thumb, 300, 200) if (url_image is not None and url_image != ""): path = self.__download(url_image, 'thumb_' + str(key)) self.emit('download-cover', key, path) def download_from_url(self, name_image, url_image): if (url_image is not None and url_image != ""): path = self.__download(url_image, 'thumb_' + name_image) self.emit('download-from-url', name_image, path) def play_item(self, item, shuffle=0, from_beginning=None, sort=None): if type(item) is str: item = self._server.fetchItem(item) parent_item = None if item.TYPE == "track": parent_item = item.album() playqueue = PlayQueue.create(self._server, item, shuffle=shuffle, continuous=1, parent=parent_item, sort=sort) self._player.set_playqueue(playqueue) GLib.idle_add(self.__play_item, from_beginning) def __play_item(self, from_beginning): self._player.start(from_beginning=from_beginning) def get_sync_items(self): if 'sync' in self._config: self.emit('sync-items', self._config['sync']) def remove_from_sync(self, item_key): if str(item_key) in self._config['sync']: del self._config['sync'][item_key] self.__save_config() self.get_sync_items() def add_to_sync(self, item, converted=False, max_items=None, only_unwatched=False): if 'sync' not in self._config: self._config['sync'] = {} if str(item.ratingKey) not in self._config['sync']: self._config['sync'][str(item.ratingKey)] = {} self._config['sync'][str(item.ratingKey)]['rating_key'] = str( item.ratingKey) self._config['sync'][str(item.ratingKey)]['converted'] = converted self._config['sync'][str( item.ratingKey)]['only_unwatched'] = only_unwatched if (max_items != None): self._config['sync'][str( item.ratingKey)]['max_items'] = max_items self.__save_config() self.get_sync_items() self.sync() def sync(self): if (self._sync_busy == False): self.emit('sync-status', True) path_dir = self._data_dir + '/' + self._server.machineIdentifier download_files = [] for file in os.listdir(path_dir): if file.startswith("item_"): download_files.append(file) self._sync_busy = True if 'sync' in self._config: sync = self._config['sync'].copy() for item_keys in sync: item = self._server.fetchItem(int(item_keys)) download_items = [] if (item.TYPE == 'movie' or item.TYPE == 'episode'): download_items.append(item) elif (item.TYPE == 'album' or item.TYPE == 'artist'): download_items = item.tracks() elif (item.TYPE == 'playlist'): download_items = item.items() elif (item.TYPE == 'show'): download_items = item.episodes() count = 0 for download_item in download_items: sync_bool = False if ('only_unwatched' not in sync[item_keys]): sync_bool = True elif (sync[item_keys]['only_unwatched'] == False): sync_bool = True elif (sync[item_keys]['only_unwatched'] == True and (download_item.TYPE == 'movie' or download_item.TYPE == 'episode') and not download_item.isWatched): sync_bool = True if (sync_bool == True): count = count + 1 if ('max_items' in sync[item_keys] and count > int(sync[item_keys]['max_items'])): break if (self.get_item_download_path(download_item) == None): self.__download_item( download_item, converted=sync[item_keys]['converted']) if ('item_' + str(download_item.ratingKey) in download_files): download_files.remove( 'item_' + str(download_item.ratingKey)) for file in download_files: path_file = os.path.join(path_dir, file) if os.path.exists(path_file): os.remove(path_file) self.emit('sync-status', False) self._sync_busy = False def __download_item(self, item, converted=False): path_dir = self._data_dir + '/' + self._server.machineIdentifier filename = 'item_' + str(item.ratingKey) filename_tmp = filename + '.tmp' path = path_dir + '/' + filename path_tmp = path_dir + '/' + filename_tmp self.emit('item-downloading', item, True) if not os.path.exists(path_dir): os.makedirs(path_dir) if not os.path.exists(path): if os.path.exists(path_tmp): os.remove(path_tmp) locations = [i for i in item.iterParts() if i] if (converted == False): download_url = self._server.url('%s?download=1' % locations[0].key) else: download_url = item.getStreamURL() utils.download(download_url, self._server._token, filename=filename_tmp, savepath=path_dir, session=self._server._session) os.rename(path_tmp, path) self.emit('item-downloading', item, False) def get_item_download_path(self, item): path_dir = self._data_dir + '/' + self._server.machineIdentifier filename = 'item_' + str(item.ratingKey) path = path_dir + '/' + filename if not os.path.exists(path): return None return path def mark_as_played(self, item): item.markWatched() item.reload() self.emit('item-retrieved', item) def mark_as_unplayed(self, item): item.markUnwatched() item.reload() self.emit('item-retrieved', item) def retrieve_item(self, item_key): item = self._server.fetchItem(int(item_key)) self.emit('item-retrieved', item) def path_for_download(self, prefix): path_dir = self._data_dir + '/' + self._server.machineIdentifier path = path_dir + '/' + prefix return [path_dir, path] def __download(self, url_image, prefix): paths = self.path_for_download(prefix) path_dir = paths[0] path = paths[1] if not os.path.exists(path_dir): os.makedirs(path_dir) if not os.path.exists(path): parse = urllib.parse.urlparse(url_image) auth_user = parse.username auth_passwd = parse.password password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() password_mgr.add_password(None, parse.scheme + "://" + parse.hostname, auth_user, auth_passwd) handler = urllib.request.HTTPBasicAuthHandler(password_mgr) opener = urllib.request.build_opener(handler) port = "" if parse.port != None: port = ":" + str(parse.port) url_img_combined = parse.scheme + "://" + parse.hostname + port + parse.path + "?" + parse.query img_raw = opener.open(url_img_combined) with open(path, 'w+b') as file: file.write(img_raw.read()) return path else: return path def connect_to_server(self): if (self._server_token is not None and self._server_url is not None): try: self.emit('loading', _('Connecting to ') + self._server_url + '.', True) self._server = PlexServer(self._server_url, self._server_token) self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) self.emit('connection-to-server') self.emit('loading', 'Success', False) return None except: self.emit( 'loading', _('Connecting to ') + self._server_url + _(' failed.'), True) print('custom url connection failed') servers_found = False for resource in self._account.resources(): servers_found = True if ('server' in resource.provides.split(',')): if self.connect_to_resource(resource): break if (servers_found == False): self.emit('loading', _('No servers found for this account.'), True) def connect_to_resource(self, resource): try: self.emit( 'loading', _('Connecting to ') + resource.name + '.\n' + _('There are ') + str(len(resource.connections)) + _(' connection urls.') + '\n' + _('This may take a while'), True) self._server = resource.connect(ssl=self._account.secure) self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) self.emit('connection-to-server') self.emit('loading', 'Success', False) return True except: self.emit('loading', _('Connecting to ') + resource.name + _(' failed.'), True) print('connection failed (when trying to connect to resource)') return False
class PlexAttributes(): def __init__(self, opts): self.opts = opts # command line options self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all) self.account = MyPlexAccount() # MyPlexAccount instance self.plex = PlexServer() # PlexServer instance self.total = 0 # Total objects parsed self.attrs = defaultdict(dict) # Attrs result set def run(self): starttime = time.time() self._parse_myplex() self._parse_server() self._parse_search() self._parse_library() self._parse_audio() self._parse_photo() self._parse_movie() self._parse_show() self._parse_client() self._parse_playlist() self._parse_sync() self.runtime = round((time.time() - starttime) / 60.0, 1) return self def _parse_myplex(self): self._load_attrs(self.account, 'myplex') self._load_attrs(self.account.devices(), 'myplex') for resource in self.account.resources(): self._load_attrs(resource, 'myplex') self._load_attrs(resource.connections, 'myplex') self._load_attrs(self.account.users(), 'myplex') def _parse_server(self): self._load_attrs(self.plex, 'serv') self._load_attrs(self.plex.account(), 'serv') self._load_attrs(self.plex.history()[:50], 'hist') self._load_attrs(self.plex.history()[50:], 'hist') self._load_attrs(self.plex.sessions(), 'sess') def _parse_search(self): for search in ('cre', 'ani', 'mik', 'she', 'bea'): self._load_attrs(self.plex.search(search), 'hub') def _parse_library(self): cat = 'lib' self._load_attrs(self.plex.library, cat) # self._load_attrs(self.plex.library.all()[:50], 'all') self._load_attrs(self.plex.library.onDeck()[:50], 'deck') self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add') for search in ('cat', 'dog', 'rat', 'gir', 'mou'): self._load_attrs(self.plex.library.search(search)[:50], 'srch') # TODO: Implement section search (remove library search?) # TODO: Implement section search filters def _parse_audio(self): cat = 'lib' for musicsection in self.plex.library.sections(): if musicsection.TYPE == library.MusicSection.TYPE: self._load_attrs(musicsection, cat) for artist in musicsection.all(): self._load_attrs(artist, cat) for album in artist.albums(): self._load_attrs(album, cat) for track in album.tracks(): self._load_attrs(track, cat) def _parse_photo(self): cat = 'lib' for photosection in self.plex.library.sections(): if photosection.TYPE == library.PhotoSection.TYPE: self._load_attrs(photosection, cat) for photoalbum in photosection.all(): self._load_attrs(photoalbum, cat) for photo in photoalbum.photos(): self._load_attrs(photo, cat) def _parse_movie(self): cat = 'lib' for moviesection in self.plex.library.sections(): if moviesection.TYPE == library.MovieSection.TYPE: self._load_attrs(moviesection, cat) for movie in moviesection.all(): self._load_attrs(movie, cat) def _parse_show(self): cat = 'lib' for showsection in self.plex.library.sections(): if showsection.TYPE == library.ShowSection.TYPE: self._load_attrs(showsection, cat) for show in showsection.all(): self._load_attrs(show, cat) for season in show.seasons(): self._load_attrs(season, cat) for episode in season.episodes(): self._load_attrs(episode, cat) def _parse_client(self): for device in self.account.devices(): client = self._safe_connect(device) if client is not None: self._load_attrs(client, 'myplex') for client in self.plex.clients(): self._safe_connect(client) self._load_attrs(client, 'client') def _parse_playlist(self): for playlist in self.plex.playlists(): self._load_attrs(playlist, 'pl') for item in playlist.items(): self._load_attrs(item, 'pl') playqueue = PlayQueue.create(self.plex, playlist) self._load_attrs(playqueue, 'pq') def _parse_sync(self): # TODO: Get plexattrs._parse_sync() working. pass def _load_attrs(self, obj, cat=None): if isinstance(obj, (list, tuple)): return [self._parse_objects(item, cat) for item in obj] self._parse_objects(obj, cat) def _parse_objects(self, obj, cat=None): clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__) clsname = clsname.replace('plexapi.', '') if self.clsnames and clsname not in self.clsnames: return None self._print_the_little_dot() if clsname not in self.attrs: self.attrs[clsname] = copy.deepcopy(NAMESPACE) self.attrs[clsname]['total'] += 1 self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples'], self.attrs[clsname]['categories'], cat) self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'], self.attrs[clsname]['docs']) def _print_the_little_dot(self): self.total += 1 if not self.total % 100: sys.stdout.write('.') if not self.total % 8000: sys.stdout.write('\n') sys.stdout.flush() def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat): if elem is None: return None for attr in sorted(elem.attrib.keys()): attrs[attr] += 1 if cat: categories[attr].add(cat) if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) for subelem in elem: attrname = TAGATTRS.get(subelem.tag, '%ss' % subelem.tag.lower()) attrs['%s[]' % attrname] += 1 def _load_obj_attrs(self, clsname, obj, attrs, docs): if clsname in STOP_RECURSING_AT: return None if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD: self._safe_reload(obj) alldocs = '\n\n'.join(self._all_docs(obj.__class__)) for attr, value in obj.__dict__.items(): if value is None or isinstance(value, (str, bool, float, int, datetime)): if not attr.startswith('_') and attr not in IGNORES.get(clsname, []): attrs[attr] += 1 if re.search('\s{8}%s\s\(.+?\)\:' % attr, alldocs) is not None: docs[attr] += 1 if isinstance(value, list): if not attr.startswith('_') and attr not in IGNORES.get(clsname, []): if value and isinstance(value[0], PlexObject): attrs['%s[]' % attr] += 1 [self._parse_objects(obj) for obj in value] def _all_docs(self, cls, docs=None): import inspect docs = docs or [] if cls.__doc__ is not None: docs.append(cls.__doc__) for parent in inspect.getmro(cls): if parent != cls: docs += self._all_docs(parent) return docs def print_report(self): total_attrs = 0 for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): meta = self.attrs[clsname] count = meta['total'] print(_('\n%s (%s)\n%s' % (clsname, count, '-' * 30), 'yellow')) attrs = sorted(set(list(meta['xml'].keys()) + list(meta['obj'].keys()))) for attr in attrs: state = self._attr_state(clsname, attr, meta) count = meta['xml'].get(attr, 0) categories = ','.join(meta['categories'].get(attr, ['--'])) examples = '; '.join(list(meta['examples'].get(attr, ['--']))[:3])[:80] print('%7s %3s %-30s %-20s %s' % (count, state, attr, categories, examples)) total_attrs += count print(_('\nSUMMARY\n%s' % ('-' * 30), 'yellow')) print('%7s %3s %3s %3s %-20s %s' % ('total', 'new', 'old', 'doc', 'categories', 'clsname')) for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): print('%7s %12s %12s %12s %s' % (self.attrs[clsname]['total'], _(self.attrs[clsname]['new'] or '', 'cyan'), _(self.attrs[clsname]['old'] or '', 'red'), _(self.attrs[clsname]['doc'] or '', 'purple'), clsname)) print('\nPlex Version %s' % self.plex.version) print('PlexAPI Version %s' % plexapi.VERSION) print('Total Objects %s' % sum([x['total'] for x in self.attrs.values()])) print('Runtime %s min\n' % self.runtime) def _clsname_match(self, clsname): if not self.clsnames: return True for cname in self.clsnames: if cname.lower() in clsname.lower(): return True return False def _attr_state(self, clsname, attr, meta): if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): self.attrs[clsname]['new'] += 1 return _('new', 'blue') if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['old'] += 1 return _('old', 'red') if attr not in meta['docs'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['doc'] += 1 return _('doc', 'purple') return _(' ', 'green') def _safe_connect(self, elem): try: return elem.connect() except: return None def _safe_reload(self, elem): try: elem.reload() except: pass
class PlexAttributes(): def __init__(self, opts): self.opts = opts # command line options self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all) self.account = MyPlexAccount() # MyPlexAccount instance self.plex = PlexServer() # PlexServer instance self.total = 0 # Total objects parsed self.attrs = defaultdict(dict) # Attrs result set def run(self): starttime = time.time() self._parse_myplex() self._parse_server() self._parse_search() self._parse_library() self._parse_audio() self._parse_photo() self._parse_movie() self._parse_show() self._parse_client() self._parse_playlist() self._parse_sync() self.runtime = round((time.time() - starttime) / 60.0, 1) return self def _parse_myplex(self): self._load_attrs(self.account, 'myplex') self._load_attrs(self.account.devices(), 'myplex') for resource in self.account.resources(): self._load_attrs(resource, 'myplex') self._load_attrs(resource.connections, 'myplex') self._load_attrs(self.account.users(), 'myplex') def _parse_server(self): self._load_attrs(self.plex, 'serv') self._load_attrs(self.plex.account(), 'serv') self._load_attrs(self.plex.history()[:50], 'hist') self._load_attrs(self.plex.history()[50:], 'hist') self._load_attrs(self.plex.sessions(), 'sess') def _parse_search(self): for search in ('cre', 'ani', 'mik', 'she', 'bea'): self._load_attrs(self.plex.search(search), 'hub') def _parse_library(self): cat = 'lib' self._load_attrs(self.plex.library, cat) # self._load_attrs(self.plex.library.all()[:50], 'all') self._load_attrs(self.plex.library.onDeck()[:50], 'deck') self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add') for search in ('cat', 'dog', 'rat', 'gir', 'mou'): self._load_attrs(self.plex.library.search(search)[:50], 'srch') # TODO: Implement section search (remove library search?) # TODO: Implement section search filters def _parse_audio(self): cat = 'lib' for musicsection in self.plex.library.sections(): if musicsection.TYPE == library.MusicSection.TYPE: self._load_attrs(musicsection, cat) for artist in musicsection.all(): self._load_attrs(artist, cat) for album in artist.albums(): self._load_attrs(album, cat) for track in album.tracks(): self._load_attrs(track, cat) def _parse_photo(self): cat = 'lib' for photosection in self.plex.library.sections(): if photosection.TYPE == library.PhotoSection.TYPE: self._load_attrs(photosection, cat) for photoalbum in photosection.all(): self._load_attrs(photoalbum, cat) for photo in photoalbum.photos(): self._load_attrs(photo, cat) def _parse_movie(self): cat = 'lib' for moviesection in self.plex.library.sections(): if moviesection.TYPE == library.MovieSection.TYPE: self._load_attrs(moviesection, cat) for movie in moviesection.all(): self._load_attrs(movie, cat) def _parse_show(self): cat = 'lib' for showsection in self.plex.library.sections(): if showsection.TYPE == library.ShowSection.TYPE: self._load_attrs(showsection, cat) for show in showsection.all(): self._load_attrs(show, cat) for season in show.seasons(): self._load_attrs(season, cat) for episode in season.episodes(): self._load_attrs(episode, cat) def _parse_client(self): for device in self.account.devices(): client = self._safe_connect(device) if client is not None: self._load_attrs(client, 'myplex') for client in self.plex.clients(): self._safe_connect(client) self._load_attrs(client, 'client') def _parse_playlist(self): for playlist in self.plex.playlists(): self._load_attrs(playlist, 'pl') for item in playlist.items(): self._load_attrs(item, 'pl') playqueue = PlayQueue.create(self.plex, playlist) self._load_attrs(playqueue, 'pq') def _parse_sync(self): # TODO: Get plexattrs._parse_sync() working. pass def _load_attrs(self, obj, cat=None): if isinstance(obj, (list, tuple)): return [self._parse_objects(item, cat) for item in obj] self._parse_objects(obj, cat) def _parse_objects(self, obj, cat=None): clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__) clsname = clsname.replace('plexapi.', '') if self.clsnames and clsname not in self.clsnames: return None self._print_the_little_dot() if clsname not in self.attrs: self.attrs[clsname] = copy.deepcopy(NAMESPACE) self.attrs[clsname]['total'] += 1 self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples'], self.attrs[clsname]['categories'], cat) self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'], self.attrs[clsname]['docs']) def _print_the_little_dot(self): self.total += 1 if not self.total % 100: sys.stdout.write('.') if not self.total % 8000: sys.stdout.write('\n') sys.stdout.flush() def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat): if elem is None: return None for attr in sorted(elem.attrib.keys()): attrs[attr] += 1 if cat: categories[attr].add(cat) if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) for subelem in elem: attrname = TAGATTRS.get(subelem.tag, '%ss' % subelem.tag.lower()) attrs['%s[]' % attrname] += 1 def _load_obj_attrs(self, clsname, obj, attrs, docs): if clsname in STOP_RECURSING_AT: return None if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD: self._safe_reload(obj) alldocs = '\n\n'.join(self._all_docs(obj.__class__)) for attr, value in obj.__dict__.items(): if value is None or isinstance(value, (str, bool, float, int, datetime)): if not attr.startswith('_') and attr not in IGNORES.get( clsname, []): attrs[attr] += 1 if re.search('\s{8}%s\s\(.+?\)\:' % attr, alldocs) is not None: docs[attr] += 1 if isinstance(value, list): if not attr.startswith('_') and attr not in IGNORES.get( clsname, []): if value and isinstance(value[0], PlexObject): attrs['%s[]' % attr] += 1 [self._parse_objects(obj) for obj in value] def _all_docs(self, cls, docs=None): import inspect docs = docs or [] if cls.__doc__ is not None: docs.append(cls.__doc__) for parent in inspect.getmro(cls): if parent != cls: docs += self._all_docs(parent) return docs def print_report(self): total_attrs = 0 for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): meta = self.attrs[clsname] count = meta['total'] print(_('\n%s (%s)\n%s' % (clsname, count, '-' * 30), 'yellow')) attrs = sorted( set(list(meta['xml'].keys()) + list(meta['obj'].keys()))) for attr in attrs: state = self._attr_state(clsname, attr, meta) count = meta['xml'].get(attr, 0) categories = ','.join(meta['categories'].get(attr, ['--'])) examples = '; '.join( list(meta['examples'].get(attr, ['--']))[:3])[:80] print('%7s %3s %-30s %-20s %s' % (count, state, attr, categories, examples)) total_attrs += count print(_('\nSUMMARY\n%s' % ('-' * 30), 'yellow')) print('%7s %3s %3s %3s %-20s %s' % ('total', 'new', 'old', 'doc', 'categories', 'clsname')) for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): print('%7s %12s %12s %12s %s' % (self.attrs[clsname]['total'], _(self.attrs[clsname]['new'] or '', 'cyan'), _(self.attrs[clsname]['old'] or '', 'red'), _(self.attrs[clsname]['doc'] or '', 'purple'), clsname)) print('\nPlex Version %s' % self.plex.version) print('PlexAPI Version %s' % plexapi.VERSION) print('Total Objects %s' % sum([x['total'] for x in self.attrs.values()])) print('Runtime %s min\n' % self.runtime) def _clsname_match(self, clsname): if not self.clsnames: return True for cname in self.clsnames: if cname.lower() in clsname.lower(): return True return False def _attr_state(self, clsname, attr, meta): if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): self.attrs[clsname]['new'] += 1 return _('new', 'blue') if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['old'] += 1 return _('old', 'red') if attr not in meta['docs'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['doc'] += 1 return _('doc', 'purple') return _(' ', 'green') def _safe_connect(self, elem): try: return elem.connect() except: return None def _safe_reload(self, elem): try: elem.reload() except: pass
class PlexAttributes(): def __init__(self, opts): self.opts = opts # command line options self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all) self.account = MyPlexAccount.signin() # MyPlexAccount instance self.plex = PlexServer() # PlexServer instance self.attrs = defaultdict(dict) # Attrs result set def run(self): # MyPlex self._load_attrs(self.account) self._load_attrs(self.account.devices()) for resource in self.account.resources(): self._load_attrs(resource) self._load_attrs(resource.connections) self._load_attrs(self.account.users()) # Server self._load_attrs(self.plex) self._load_attrs(self.plex.account()) self._load_attrs(self.plex.history()[:20]) self._load_attrs(self.plex.playlists()) for search in ('cre', 'ani', 'mik', 'she'): self._load_attrs(self.plex.search('cre')) self._load_attrs(self.plex.sessions()) # Library self._load_attrs(self.plex.library) self._load_attrs(self.plex.library.sections()) self._load_attrs(self.plex.library.all()[:20]) self._load_attrs(self.plex.library.onDeck()[:20]) self._load_attrs(self.plex.library.recentlyAdded()[:20]) for search in ('cat', 'dog', 'rat'): self._load_attrs(self.plex.library.search(search)[:20]) # Client self._load_attrs(self.plex.clients()) return self def _load_attrs(self, obj): if isinstance(obj, (list, tuple)): return [self._parse_objects(x) for x in obj] return self._parse_objects(obj) def _parse_objects(self, obj): clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__) clsname = clsname.replace('plexapi.', '') if self.clsnames and clsname not in self.clsnames: return None sys.stdout.write('.') sys.stdout.flush() if clsname not in self.attrs: self.attrs[clsname] = copy.deepcopy(NAMESPACE) self.attrs[clsname]['total'] += 1 self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples']) self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj']) def _load_xml_attrs(self, clsname, elem, attrs, examples): if elem in (None, NA): return None for attr in sorted(elem.attrib.keys()): attrs[attr] += 1 if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) def _load_obj_attrs(self, clsname, obj, attrs): for attr, value in obj.__dict__.items(): if value in (None, NA) or isinstance( value, (str, bool, float, int, datetime)): if not attr.startswith('_') and attr not in IGNORES.get( clsname, []): attrs[attr] += 1 def print_report(self): total_attrs = 0 for clsname in sorted(self.attrs.keys()): meta = self.attrs[clsname] count = meta['total'] print( _('\n%s (%s)\n%s' % (clsname, count, '-' * (len(clsname) + 8)), 'yellow')) attrs = sorted( set(list(meta['xml'].keys()) + list(meta['obj'].keys()))) for attr in attrs: state = self._attr_state(attr, meta) count = meta['xml'].get(attr, 0) example = list(meta['examples'].get(attr, ['--']))[0][:80] print('%-4s %4s %-30s %s' % (state, count, attr, example)) total_attrs += count print(_('\nSUMMARY\n------------', 'yellow')) print('Plex Version %s' % self.plex.version) print('PlexAPI Version %s' % plexapi.VERSION) print('Total Objects %s\n' % sum([x['total'] for x in self.attrs.values()])) for clsname in sorted(self.attrs.keys()): print('%-34s %s' % (clsname, self.attrs[clsname]['total'])) print() def _attr_state(self, attr, meta): if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): return _('new', 'blue') if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): return _('old', 'red') return _(' ', 'green')