def _addExternalSubtitles(self, plexServer: PlexServer):
        """Add external subtitles to the player

        :param plexServer: Plex server to get subtitles from
        :type plexServer: :class:`PlexServer`
        """
        if not self._item:
            return

        # note: internal subtitles don't have a key provided by plexapi
        external_subtitles = [
            sub for sub in self._item.subtitleStreams() if sub.key
        ]
        if external_subtitles:
            for subtitle in external_subtitles:
                # TODO: What to do with forced subs?
                self.addSubtitle(
                    plexServer.url(subtitle.key, includeToken=True),
                    subtitle.title if subtitle.title else SUBTITLE_UNKNOWN,
                    subtitle.language if subtitle.language else
                    SUBTITLE_UNKNOWN, subtitle.selected)
                log((
                    f"external subtitle '{subtitle.title}' [{subtitle.language}]"
                    f"at index {subtitle.index} added for '{self._item.title}' ({self._file})"
                    f"from media provider {mediaProvider2str(self._mediaProvider)}"
                ), xbmc.LOGINFO)
Exemple #2
0
    def getStreamUrl(mediaPart: media.MediaPart,
                     plexServer: server.PlexServer) -> str:
        if not mediaPart:
            raise ValueError('invalid mediaPart')
        if not plexServer:
            raise ValueError('invalid plexServer')

        return plexServer.url(mediaPart.key, includeToken=True)
Exemple #3
0
class PlexBackend(pykka.ThreadingActor, backend.Backend):
    def __init__(self, config, audio):
        super(PlexBackend, self).__init__(audio=audio)
        self.config = config
        self.session = get_requests_session(
            proxy_config=config['proxy'],
            user_agent='%s/%s' %
            (mopidy_plex.Extension.dist_name, mopidy_plex.__version__))
        baseurl = (config['plex']['server'])
        token = (config['plex']['token'])
        self.plex = PlexServer(baseurl, token)
        self.music = [
            s for s in self.plex.library.sections()
            if s.TYPE == MusicSection.TYPE
        ][0]
        logger.debug('Found music section on plex server %s: %s', self.plex,
                     self.music)
        self.library_id = config['plex']['library_id']
        self.uri_schemes = [
            'plex',
        ]
        self.library = PlexLibraryProvider(backend=self)
        self.playback = PlexPlaybackProvider(audio=audio, backend=self)

    def plex_uri(self, uri_path, prefix='plex'):
        '''Get a leaf uri and complete it to a mopidy plex uri.

        E.g. plex:artist:3434
             plex:track:2323
             plex:album:2323
             plex:playlist:3432
        '''
        uri_path = str(uri_path)
        if not uri_path.startswith('/library/metadata/'):
            uri_path = '/library/metadata/' + uri_path

        if uri_path.startswith('/library/metadata/'):
            uri_path = uri_path[len('/library/metadata/'):]
        return '{}:{}'.format(prefix, uri_path)

    def resolve_uri(self, uri_path):
        '''Get a leaf uri and return full address to plex server'''
        uri_path = str(uri_path)
        if not uri_path.startswith('/library/metadata/'):
            uri_path = '/library/metadata/' + uri_path
        return self.plex.url(uri_path)
Exemple #4
0
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
Exemple #5
0
                name="Photos",
                type="photo",
                location="/data/Photos"
                if opts.no_docker is False else photos_path,
                agent="com.plexapp.agents.none",
                scanner="Plex Photo Scanner",
                expected_media_count=has_photos,
            ))

    # Create the Plex library in our instance
    if sections:
        print("Creating the Plex libraries on %s" % server.friendlyName)
        for section in sections:
            create_section(server, section, opts)

    # Share this instance with the specified username
    if account:
        shared_username = os.environ.get("SHARED_USERNAME", "PKKid")
        try:
            user = account.user(shared_username)
            account.updateFriend(user, server)
            print("The server was shared with user %s" % shared_username)
        except NotFound:
            pass

    # Finished: Display our Plex details
    print("Base URL is %s" % server.url("", False))
    if account and opts.show_token:
        print("Auth token is %s" % account.authenticationToken)
    print("Server %s is ready to use!" % opts.server_name)
Exemple #6
0
class PlexSleep:
    """ Use the plexapi to monitor:
            - client connections
            - streaming sessions
            - transcoding sessions (for things like sync)

    """
    config_filename = 'config_plex_sleep.yml'
    def __init__(self, config_filename):

        log.info(f'plex_sleep v{__version__}')
        self.load_config(config_filename)
        self.activity = time.time()
        self.baseurl = f'http://{self.server}:{self.port}'

        self.wait_for_resume()
        log.info(f"Connnecting to plex server at {self.baseurl}...")
        self.plex = PlexServer(self.baseurl, self.token)
        log.info(f"Connnected")

        self.pending_refreshes = {}
        self.watch_server()

    def load_config(self, filename):
        with open(filename) as f:
            self.config = yaml.load(f, Loader=yaml.FullLoader)

        self.user = self.config.get('user')
        self.server = self.config.get('server', 'localhost')
        self.port = self.config.get('port', '32400')
        self.timeout = self.config.get('timeout', 10*60)   # Default to 10 minutes before sleeping
        self.check_interval = self.config.get('check_interval', 60)  # Check every minute to update who's connected

        # Default scan intervals
        self.library_scan_interval = {'movie': 60*60*12,     # 12 hours,
                                      'show':  60*60*12,     # TV shows = 12 hours
                                      'artist': 60*60*48,    # Music = 2 days
                                      'photo': 60*60*24,     # Photos = 1 day,
                                      }
        scan_intervals = self.config.get('scan_interval', "movie:43200")
        for entry in scan_intervals.split(','):
            lib_type, lib_interval = entry.split(':')
            lib_type = lib_type.strip()
            lib_interval = lib_interval.strip()
            self.library_scan_interval[lib_type] = int(lib_interval)
            # Catch some common errors that a user might make
            if lib_type == 'music' or lib_type == 'mp3':
                log.warn(f'For scan_interval, you have used {lib_type}, but please use "artist" for Music libraries')
            if lib_type == 'tv' or lib_type == 'tv shows':
                log.warn(f'For scan_interval, you have used {lib_type}, but please use "show" for TV libraries')
        
        for lib_type, lib_interval in self.library_scan_interval.items():
            log.info(f'Scan interval for {lib_type} libraries: {self.library_scan_interval[lib_type]} seconds')

        # Get the token from the environment first, then look for it in the config file
        if 'PLEX_TOKEN' in os.environ:
            self.token = os.environ.get('PLEX_TOKEN')
        elif 'token' in self.config:
            self.token = self.config.get('token')
        else:
            log.error(f'No PLEX_TOKEN environment variable set or "token" statement in config file (used to connect to Plex server')
            log.error('Exiting')
            sys.exit(-1)

    def watch_server(self):
        idle_time = 0
        last_client_time = time.time()
        log.info('Started monitoring')
        while True:
            n_clients = self.get_num_clients()
            n_sess = self.get_num_sessions()
            n_trans = self.get_num_transcode_sessions()
            n_activity = self.get_activity_report()
            n = n_clients + n_sess + n_trans + n_activity
            self.refresh_libraries()

            if n>0: # People are browsing the server
                last_client_time = time.time()
                log.debug(f'Active clients:{n_clients}|sessions:{n_sess}|transcodes:{n_trans}|scans:{n_activity}')
            else:
                idle_time = time.time() - last_client_time
                if idle_time > self.timeout:
                    log.info(f'Plex server idle for {int(idle_time/60)} minutes. Suspending...')
                    if self._is_alive(self.server):
                        os.system(f"""ssh -o StrictHostKeyChecking=no {self.user}@{self.server} 'echo "sudo pm-suspend" | at now + 1 minute'""")
                    self.wait_for_suspend()
                    self.wait_for_resume()
                    log.info('resuming...')
                    last_client_time = time.time()
                else:
                    log.debug(f'Plex server has been idle for {int(idle_time/60)} minutes')
            time.sleep(self.check_interval)

    def _is_alive(self, server):
        r = ping(server, count=1, timeout=1)
        return r.success()

    def wait_for_suspend(self):
        log.info(f'waiting for {self.server} to sleep')
        while True:
            if self._is_alive(self.server):
                log.debug('ping is alive')
                time.sleep(5)
            else:
                log.info(f'{self.server} is asleep')
                return

    def wait_for_resume(self):
        log.info(f'waiting for {self.server} to awaken')
        while True:
            try:
                if self._is_alive(self.server):
                    log.info(f'{self.server} is awake')
                    return
                else:
                    raise OSError
            except OSError:
                log.debug('ping is dead, waiting...')
                time.sleep(self.check_interval)
                # Probably errno 64 Host is down
                
    def _json_query(self, end_point):
        """
            Make a custom query that returns json instead of xml like Plex's default
            end_point:  '/status/sessions'
        """
        headers = self.plex._headers()
        headers['Accept'] = 'application/json'
        url = self.plex.url(end_point)
        response = requests.get(url, headers=headers)
        return response.text

    def get_num_sessions(self):
        # Any client/app watching a stream
        return self._parse_count('/status/sessions')
        
    def get_num_clients(self):
        # Any client/app browsing the server
        return self._parse_count('/clients/')

    def get_num_transcode_sessions(self):
        # Transcodes to a player or a sync
        return self._parse_count('/transcode/sessions')

    def get_activity_report(self):
        # Any library scans running on the server are reported here
        return self._parse_count('/activities')

    def _parse_count(self, end_point):
        if self._is_alive(self.server):
            j = self._json_query(end_point)
            d = json.loads(j)
            log.debug(json.dumps(d, indent=4) )
            return int(d['MediaContainer']['size'])
        else:
            return 0

    def refresh_libraries(self):
        """Trigger a rescan of any library that was last scanned earlier than
           our library_scan_interval
        """

        # Get the library api
        end_point = '/library/sections'
        j = self._json_query(end_point)
        d = json.loads(j)
        log.debug(json.dumps(d, indent=4) )
        current_time = int(time.time())

        # Clear out any pending refresh marks that are older than 10 minutes
        #   - This is for the corner case where we marked something for refresh, it started refreshing
        #     and then it finished refreshing before this function was called again.
        #for r, start_time in dict(self.pending_refreshes).items():
            #if (current_time - start_time) > 10*60:
                #del self.pending_refreshes[r]

        for library in d['MediaContainer']['Directory']:
            last_scan = library['scannedAt']
            library_type = library['type']
            # Don't do anything if the scan interval is zero
            if self.library_scan_interval[library_type] == 0: continue

            if not library['refreshing']:
                # If Plex is not currently refreshing
                if current_time - last_scan > self.library_scan_interval.get(library_type, 60*60*24):
                    # Library last refreshed earlier than library scan interval, so mark for refresh
                    if library['key'] not in self.pending_refreshes:
                        log.info(f'Starting refresh of {library["title"]}')
                        end_point = f'/library/sections/{library["key"]}/refresh'
                        j = self._json_query(end_point)
                        self.pending_refreshes[library['key']] = current_time
                    else:
                        # We've already queued this up for a refresh so don't do anything
                        pass
                else:
                    if library['key'] in self.pending_refreshes:
                        log.info(f'Completed refresh of {library["title"]}')
                        del self.pending_refreshes[library['key']]
Exemple #7
0
import os
from plexapi.server import PlexServer
from plexapi import utils

baseurl = 'https://plx.w00t.cloud'
token = 'H6gqeSNE3yGthe72x1w7'
plex = PlexServer(baseurl, token)

playlists = [pl for pl in plex.playlists()]
playlist = utils.choose('Choose Playlist', playlists,
                        lambda pl: '%s' % pl.title)

print(len(playlist.items()))
for photo in playlist.items():
    photomediapart = photo.media[0].parts[0]
    print('Download File: %s' % photomediapart.file)
    url = plex.url('%s?download=1' % photomediapart.key)
    utils.download(url, token, os.path.basename(photomediapart.file))
        print('Finished with photos...')
        sections.append(
            dict(name='Photos',
                 type='photo',
                 location='/data/Photos',
                 agent='com.plexapp.agents.none',
                 scanner='Plex Photo Scanner',
                 expected_media_count=has_photos))

    if sections:
        print('Ok, got the media, it`s time to create a library for you!')

        for section in sections:
            create_section(server, section)

    if account:
        shared_username = os.environ.get('SHARED_USERNAME', 'PKKid')
        try:
            user = account.user(shared_username)
            account.updateFriend(user, server)
            print('The server was shared with user "%s"' % shared_username)
        except NotFound:
            pass

    print('Base URL is %s' % server.url('', False))
    if account and opts.show_token:
        print('Auth token is %s' % account.authenticationToken)

    print('Server %s is ready to use!' % opts.server_name)
Exemple #9
0
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")
Exemple #10
0
    def fillVideoInfos(plexServer: server.PlexServer,
                       itemId: int,
                       plexItem: video.Video,
                       mediaType: str,
                       item: ListItem,
                       allowDirectPlay: bool = False):
        """
        Populate the provided ListItem object with existing data from plexItem
        and additional detail pulled from the provided plexServer

        :param plexServer: Plex server to gather additional details from
        :type plexServer: server.PlexServer
        :param itemId: Unique ID of the plex Video object item
        :type itemId: int
        :param plexItem: Plex object populated with information about the item
        :type plexItem: video.Video
        :param mediaType: Kodi Media type object
        :type mediaType: str
        :param item: Instantiated Kodi ListItem to populate with additional details
        :type item: :class:`ListItem`
        :param allowDirectPlay: Settings definition on provider if directPlay is allowed
        :type allowDirectPlay: bool, optional
        """
        videoInfoTag = item.getVideoInfoTag()

        videoInfoTag.setMediaType(mediaType)
        videoInfoTag.setTitle(item.getLabel() or '')

        date = None
        isFolder = False

        resumeTime = 0.0
        duration = 0.0

        artwork = {}
        collections = []
        media = []
        locations = []
        roles = []

        if isinstance(plexItem, video.Video):
            videoInfoTag.setSortTitle(plexItem.titleSort or '')
            videoInfoTag.setPlot(plexItem.summary or '')
            videoInfoTag.setDateAdded(
                Api.convertDateTimeToDbDateTime(plexItem.addedAt))
            videoInfoTag.setPlaycount(plexItem.viewCount or 0)
            videoInfoTag.setLastPlayed(
                Api.convertDateTimeToDbDateTime(plexItem.lastViewedAt))
            videoInfoTag.setTags([plexItem.librarySectionTitle])

        if isinstance(plexItem, video.Movie):
            date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt)
            duration = Api.MillisecondsToSeconds(plexItem.duration)
            resumeTime = Api.MillisecondsToSeconds(plexItem.viewOffset)
            collections = plexItem.collections or []
            media = plexItem.media or []
            roles = plexItem.roles or []

            videoInfoTag.setMpaa(plexItem.contentRating or '')
            videoInfoTag.setDuration(int(duration))
            videoInfoTag.setOriginalTitle(plexItem.originalTitle or '')
            videoInfoTag.setPremiered(date)
            videoInfoTag.setRating(plexItem.rating or 0.0)
            videoInfoTag.setTagLine(plexItem.tagline or '')
            videoInfoTag.setUserRating(int(plexItem.userRating or 0))
            videoInfoTag.setYear(plexItem.year or 0)
            videoInfoTag.setStudios(Api.ListFromString(plexItem.studio))
            videoInfoTag.setCountries(Api.ListFromMediaTags(
                plexItem.countries))
            videoInfoTag.setGenres(Api.ListFromMediaTags(plexItem.genres))
            videoInfoTag.setDirectors(Api.ListFromMediaTags(
                plexItem.directors))
            videoInfoTag.setWriters(Api.ListFromMediaTags(plexItem.writers))
        elif isinstance(plexItem, collection.Collection):
            # ignore empty collections
            if plexItem.childCount <= 0:
                return

            isFolder = True

            videoInfoTag.setPlot(plexItem.summary or '')
            videoInfoTag.setDateAdded(
                Api.convertDateTimeToDbDateTime(plexItem.addedAt))
        elif isinstance(plexItem, video.Show):
            isFolder = True
            date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt)
            duration = Api.MillisecondsToSeconds(plexItem.duration)
            locations = plexItem.locations or []
            collections = plexItem.collections or []
            roles = plexItem.roles or []

            banner = plexItem.banner
            if banner:
                artwork['banner'] = plexServer.url(banner, includeToken=True)

            videoInfoTag.setMpaa(plexItem.contentRating or '')
            videoInfoTag.setDuration(int(duration))
            videoInfoTag.setOriginalTitle(plexItem.originalTitle or '')
            videoInfoTag.setPremiered(date)
            videoInfoTag.setRating(plexItem.rating or 0.0)
            videoInfoTag.setTagLine(plexItem.tagline or '')
            videoInfoTag.setYear(plexItem.year or 0)
            videoInfoTag.setStudios(Api.ListFromString(plexItem.studio))
            videoInfoTag.setGenres(Api.ListFromMediaTags(plexItem.genres))
        elif isinstance(plexItem, video.Season):
            isFolder = True

            videoInfoTag.setTvShowTitle(plexItem.parentTitle or '')
            videoInfoTag.setSeason(plexItem.index)
        elif isinstance(plexItem, video.Episode):
            date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt)
            resumeTime = Api.MillisecondsToSeconds(plexItem.viewOffset)
            duration = Api.MillisecondsToSeconds(plexItem.duration)
            media = plexItem.media or []

            videoInfoTag.setTvShowTitle(plexItem.grandparentTitle or '')
            videoInfoTag.setSeason(int(plexItem.parentIndex))
            videoInfoTag.setEpisode(plexItem.index)
            videoInfoTag.setMpaa(plexItem.contentRating or '')
            videoInfoTag.setDuration(int(duration))
            videoInfoTag.setFirstAired(date)
            videoInfoTag.setRating(plexItem.rating or 0.0)
            videoInfoTag.setYear(plexItem.year or 0)
            videoInfoTag.setDirectors(Api.ListFromMediaTags(
                plexItem.directors))
            videoInfoTag.setWriters(Api.ListFromMediaTags(plexItem.writers))

        # handle collections / sets
        collections = Api.ListFromMediaTags(collections)
        if collections:
            # Kodi can only store one set per media item
            videoInfoTag.setSet(collections[0])

        # set the item's datetime if available
        if date:
            item.setDateTime(date)

        # specify whether the item is a folder or not
        item.setIsFolder(isFolder)

        # add the item's ID as a unique ID belonging to Plex
        uniqueIDs = {PLEX_PROTOCOL: str(itemId)}
        # retrieve and map GUIDS from Plex
        if isinstance(plexItem,
                      (video.Movie, video.Show, video.Season, video.Episode)):
            guids = Api._mapGuids(plexItem.guids)
            if guids:
                uniqueIDs = {**guids, **uniqueIDs}

        videoInfoTag.setUniqueIDs(uniqueIDs, PLEX_PROTOCOL)

        # handle actors / cast
        cast = []
        for index, role in enumerate(roles):
            actor = xbmc.Actor(role.tag.strip(), (role.role or '').strip(),
                               index, role.thumb)
            cast.append(actor)
        if cast:
            videoInfoTag.setCast(cast)

        # handle resume point
        if resumeTime > 0 and duration > 0.0:
            videoInfoTag.setResumePoint(resumeTime, duration)

        # handle stream details
        path = None
        for mediaStream in media:
            for part in mediaStream.parts:
                # pick the first MediaPart with a valid file and stream URL
                if not path and part.file and part.key:
                    path = part.file

                for videoStream in part.videoStreams():
                    videoInfoTag.addVideoStream(
                        xbmc.VideoStreamDetail(width=videoStream.width or 0,
                                               height=videoStream.height or 0,
                                               codec=videoStream.codec or '',
                                               duration=int(duration),
                                               language=videoStream.language
                                               or ''))

                for audioStream in part.audioStreams():
                    videoInfoTag.addAudioStream(
                        xbmc.AudioStreamDetail(channels=audioStream.channels
                                               or 2,
                                               codec=audioStream.codec or '',
                                               language=audioStream.language
                                               or ''))

                for index, subtitleStream in enumerate(part.subtitleStreams()):
                    videoInfoTag.addSubtitleStream(
                        xbmc.SubtitleStreamDetail(
                            language=subtitleStream.language or f"[{index}]"))

        if isFolder:
            # for folders use locations for the path
            if locations:
                path = locations[0]
            item.setPath(plexServer.url(plexItem.key, includeToken=True))
        else:
            # determine if directPlay is enabled and possible
            if allowDirectPlay:
                directPlayUrl = Api.getDirectPlayUrlFromPlexItem(plexItem)
                if directPlayUrl:
                    item.setPath(directPlayUrl)

            # otherwise determine the stream URL
            if not item.getPath():
                item.setPath(Api.getStreamUrlFromPlexItem(
                    plexItem, plexServer))

        if path:
            videoInfoTag.setPath(path)
        videoInfoTag.setFilenameAndPath(item.getPath())

        # handle artwork
        poster = None
        fanart = None
        if isinstance(plexItem, video.Video):
            poster = plexItem.thumbUrl
            fanart = plexItem.artUrl
        elif isinstance(plexItem, collection.Collection) and plexItem.thumb:
            poster = plexServer.url(plexItem.thumb, includeToken=True)

        if poster:
            artwork['poster'] = poster
        if fanart:
            artwork['fanart'] = fanart
        if artwork:
            item.setArt(artwork)
Exemple #11
0
class PlexBackend(pykka.ThreadingActor, backend.Backend):
    def __init__(self, config, audio):
        super(PlexBackend, self).__init__(audio=audio)
        self.config = config
        self.session = get_requests_session(
            proxy_config=config['proxy'],
            user_agent='%s/%s' %
            (mopidy_plex.Extension.dist_name, mopidy_plex.__version__))
        type = config['plex']['type']
        library = (config['plex']['library'])
        self.plex = None
        self.music = None
        if type == 'myplex':
            server = (config['plex']['server'])
            user = (config['plex']['username'])
            password = (config['plex']['password'])
            account = self.myplex_login(user, password)
            logger.info('Connecting to plex server: %s', server)
            self.plex = account.resource(server).connect(ssl=False)
            self.music = self.plex.library.section(library)
        elif type == 'direct':
            baseurl = (config['plex']['server'])
            token = (config['plex']['token'])
            self.plex = PlexServer(baseurl, token)
            self.music = self.plex.library.section(library)
        else:
            logger.error('Invalid value for plex backend type: %s', type)

        logger.info('Connected to plex server')
        logger.debug('Found music section on plex server %s: %s', self.plex,
                     self.music)
        self.library_id = self.music.key
        self.uri_schemes = [
            'plex',
        ]
        self.library = PlexLibraryProvider(backend=self)
        self.playback = PlexPlaybackProvider(audio=audio, backend=self)
        self.playlists = PlexPlaylistsProvider(backend=self)

    def myplex_login(self, user, password):
        max_attempts = 20
        current_attempt = 0
        account = None
        while account is None:
            try:
                account = MyPlexAccount(user, password, session=self.session)
            except Exception as e:
                if current_attempt > max_attempts:
                    logger.error(
                        'Could not connect to MyPlex in time, exiting...')
                    return None
                traceback.print_exception(*e)
                current_attempt += 1
                logger.error('Failed to log into MyPlex, retrying... %s/%s',
                             current_attempt, max_attempts)
                sleep(5)
        return account

    def plex_uri(self, uri_path, prefix='plex'):
        '''Get a leaf uri and complete it to a mopidy plex uri.

        E.g. plex:artist:3434
             plex:track:2323
             plex:album:2323
             plex:playlist:3432
        '''
        uri_path = str(uri_path)
        if not uri_path.startswith('/library/metadata/'):
            uri_path = '/library/metadata/' + uri_path

        if uri_path.startswith('/library/metadata/'):
            uri_path = uri_path[len('/library/metadata/'):]
        return '{}:{}'.format(prefix, uri_path)

    def resolve_uri(self, uri_path):
        '''Get a leaf uri and return full address to plex server'''
        uri_path = str(uri_path)
        if not uri_path.startswith('/library/metadata/'):
            uri_path = '/library/metadata/' + uri_path
        return self.plex.url(uri_path)