def cli(debug, username, password, servername, url, token, config, verify_ssl): """ Entry point for the CLI.""" global PMS global CONFIG if debug: LOG.setLevel(logging.DEBUG) else: LOG.setLevel(logging.INFO) if config and os.path.isfile(config): CONFIG = read_or_make(config) url = url or CONFIG.get('url') token = token or CONFIG.get('token') verify_ssl = verify_ssl or CONFIG.get('verify_ssl') if url and token or username and password: PMS = get_pms(url=url, token=token, username=username, password=password, servername=servername, verify_ssl=verify_ssl)
def get_pms(url=None, token=None, username=None, password=None, servername=None, verify_ssl=None): from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer url = url or CONFIG.get('url') token = token or CONFIG.get('token') verify_ssl = verify_ssl or CONFIG.get('verify_ssl', False) if url and token: sess = requests.Session() if not verify_ssl: sess.verify = False PMS = PlexServer(url, token, sess) elif username and password and servername: acc = MyPlexAccount(username, password) PMS = acc.resource(servername).connect() LOG.debug('Getting server %s', PMS.friendlyName) return PMS
def client_jump_to(offset=None, sessionkey=None): """Seek the client to the offset. Args: offset(int): Default None sessionkey(int): So we made sure we control the correct client. Returns: None """ global JUMP_LIST LOG.debug('Called jump with %s %s %s', offset, to_time(offset), sessionkey) if offset == -1: return conf_clients = CONFIG.get('clients', []) conf_users = CONFIG.get('users', []) for media in PMS.sessions(): # Find the client.. This client does not have the correct address # or 'protocolCapabilities' so we have to get the correct one. # or we can proxy thru the server.. if sessionkey and int(sessionkey) == media.sessionKey: client = media.players[0] user = media.usernames[0] LOG.debug('client %s %s %s', client.title, user, (media.viewOffset / 1000)) # Check that this client is allowed. if conf_clients and client.title not in conf_clients: LOG.debug('client %s is not allowed', client.title) return # Check that this user is allowed. if conf_users and user not in conf_users: LOG.debug('user %s is not allowed', user) return # To stop processing. from func task if we have used to much time.. # This will not work if/when credits etc are added. Need a better way. # if offset <= media.viewOffset / 1000: # LOG.debug('Didnt jump because of offset') # return # This does not work on plex web since the f****r returns # the local url.. LOG.debug('connecting to client') client = PMS.client(client.title).connect() LOG.debug('calling seekTo') client.seekTo(int(offset * 1000)) LOG.debug('Jumped %s %s to %s %s', user, client.title, offset, media._prettyfilename()) # Some clients needs some time.. # time.sleep(0.2) # client.play() JUMP_LIST.remove(sessionkey) #time.sleep(1) return
def check_real_file_access(path): if os.path.exists(path): return path for key, value in CONFIG.get('remaps', {}).items(): fp = path.replace(key, value) if os.path.exists(fp): return fp
def download_theme(media, ht, theme_source=None, url=None): if media.TYPE == 'show': name = media.title rk = media.ratingKey _theme = media.theme else: name = media.grandparentTitle rk = media.grandparentRatingKey _theme = media.grandparentTheme if _theme is None: _theme = media.show().theme pms = media._server if theme_source is None: theme_source = CONFIG.get('theme_source', 'plex') if theme_source == 'youtube': theme = search_for_theme_youtube(name, rk, THEMES, url=url) elif theme_source == 'tvtunes': theme = search_tunes(name, rk, url=url) theme = list(itertools.chain.from_iterable(theme.values())) elif theme_source == 'plex': theme = pms.url(_theme, includeToken=True) LOG.debug('Downloading theme via plex %s', theme) elif theme_source == 'all': theme = [] st = search_tunes(name, rk, url=url) st_res = list(itertools.chain.from_iterable(st.values())) theme.extend(st_res) theme.append(pms.url(_theme, includeToken=True)) theme.append(search_for_theme_youtube(name, rk, THEMES, url=url)) if not isinstance(theme, list): theme = [theme] final = [] for th in theme: LOG.debug('Download theme using source %s', th) # Filename is just added so we can pass a url to convert_and_trim th = convert_and_trim(th, fs=11025, theme=True, filename='%s__%s__%s' % (name, rk, int(time.time()))) analyzer().ingest(ht, th) final.append(th) return final
def create_edl_path(path): """Convert a file with a ext to .edl ext.""" from bw_plex import CONFIG if not os.path.exists(path): for key, value in CONFIG.get('remaps'): fp = path.replace(key, value) if os.path.exists(fp): path = fp break f_without_ext = os.path.splitext(path)[0] edl_path = f_without_ext + '.edl' return edl_path
def check_file_access(m): """Check if we can reach the file directly or if we have to download it via PMS. Args: m (plexapi.video.Episode) Return: filepath or http to the file. """ LOG.debug('Checking if we can reach %s directly', m._prettyfilename()) files = list(m.iterParts()) # Now we could get the "wrong" file here. # If the user has duplications we might return the wrong file # CBA with fixing this as it requires to much work :P # And the use case is rather slim, you should never have dupes. # If the user has they can remove them using plex-cli. for file in files: if os.path.exists(file.file): LOG.debug('Found %s', file.file) return file.file elif CONFIG.get('remaps', []): for key, value in CONFIG.get('remaps'): fp = file.file.replace(key, value) if os.path.exists(fp): LOG.debug('Found %s', fp) return fp else: LOG.warning('Downloading from pms..') try: # for plexapi 3.0.6 and above. return PMS.url('%s?download=1' % file.key, includeToken=True) except TypeError: return PMS.url('%s?download=1' % file.key)
def has_recap_audio(audio, phrase=None, thresh=1, duration=30): """ audio is wave in 16k sample rate.""" import speech_recognition as sr if phrase is None: phrase = CONFIG.get('words') try: r = sr.Recognizer() with sr.AudioFile(audio) as source: r.adjust_for_ambient_noise(source) audio = r.record(source, duration=duration) result = r.recognize_sphinx(audio, keyword_entries=[(i, thresh) for i in phrase]) LOG.debug('Found %s in audio', result) return result except sr.UnknownValueError: pass return False
def process_to_db(media, theme=None, vid=None, start=None, end=None, ffmpeg_end=None, recap=None): """Process a plex media item to the db Args: media (Episode obj): theme: path to the theme. vid: path to the stripped wav of the media item. start (None, int): of theme. end (None, int): of theme. ffmpeg_end (None, int): What does ffmpeg think is the start of the ep. Returns: None """ global HT ff = -1 name = media._prettyfilename() LOG.debug('Started to process %s', name) if theme is None: theme = get_theme(media) theme = convert_and_trim(theme, fs=11025, theme=True) if vid is None: vid = convert_and_trim(check_file_access(media), fs=11025, trim=600) # too cover manual process_to_db. if theme not in HT.names: analyzer().ingest(HT, theme) # Lets skip the start time for now. This need to be added later to support shows # that have show, theme song show. if end is None: start, end = get_offset_end(vid, HT) if ffmpeg_end is None: ffmpeg_end = find_offset_ffmpeg(check_file_access(media)) if recap is None: recap = has_recap(media, CONFIG.get('words'), audio=vid) if end is not None: with session_scope() as se: try: se.query(Preprocessed).filter_by(ratingKey=media.ratingKey).one() except NoResultFound: p = Preprocessed(show_name=media.grandparentTitle, ep_title=media.title, theme_end=end, theme_start=start, theme_start_str=to_time(start), theme_end_str=to_time(end), ffmpeg_end=ffmpeg_end, ffmpeg_end_str=to_time(ff), duration=media.duration, ratingKey=media.ratingKey, grandparentRatingKey=media.grandparentRatingKey, prettyname=media._prettyfilename(), updatedAt=media.updatedAt, has_recap=recap) se.add(p) LOG.debug('Added %s to media.db', name)
def check(data): global JUMP_LIST if data.get('type') == 'playing' and data.get( 'PlaySessionStateNotification'): sess = data.get('PlaySessionStateNotification')[0] if sess.get('state') != 'playing': return ratingkey = sess.get('ratingKey') sessionkey = int(sess.get('sessionKey')) progress = sess.get('viewOffset', 0) / 1000 # converted to sec. mode = CONFIG.get('mode', 'skip_only_theme') # This has to be removed if/when credits are added. if progress >= 600: return def best_time(item): """Find the best time in the db.""" if item.correct_theme_end and item.correct_theme_end != 1: sec = item.correct_theme_end elif item.correct_ffmpeg and item.correct_ffmpeg != 1: sec = item.correct_ffmpeg elif item.theme_end and item.theme_end != -1: sec = item.theme_end elif item.ffmpeg_end and item.ffmpeg_end != -1: sec = item.ffmpeg_end else: sec = -1 return sec def jump(item, sessionkey, sec=None): if sec is None: sec = best_time(item) if sessionkey not in JUMP_LIST: JUMP_LIST.append(sessionkey) POOL.apply_async(client_jump_to, args=(sec, sessionkey)) with session_scope() as se: try: item = se.query(Preprocessed).filter_by(ratingKey=ratingkey).one() if item: LOG.debug('Found %s theme start %s, theme end %s, progress %s', item.prettyname, item.theme_start_str, item.theme_end_str, to_time(progress)) bt = best_time(item) if mode == 'skip_if_recap' and item.theme_end and item.theme_start: return jump(item, sessionkey, bt) if mode == 'skip_only_theme': if item.correct_theme_end and item.correct_theme_start: if progress > item.correct_theme_start and progress < item.correct_theme_end: LOG.debug('%s is in the correct time range correct_theme_end', item.prettyname) return jump(item, sessionkey, item.correct_theme_end) elif item.theme_end and item.theme_start: if progress > item.theme_start and progress < item.theme_end: LOG.debug('%s is in the correct time range theme_end', item.prettyname) return jump(item, sessionkey, item.theme_end) #if progress > item.theme_start and progress < item.theme_end: # LOG.debug('%s is in the correct time range', item.prettyname) # return jump(item, sessionkey, item.correct_theme_end or item.theme_end) #else: # if item.theme_start - progress < 0: # n = item.theme_start - progress # if n > 0: # part = 'jumping in %s' % to_time(n) # else: # part = 'should have jumped %s ago' % to_time(n) # LOG.debug('Skipping %s as it not in the correct time range jumping in %s', # item.prettyname, part) except NoResultFound: if ratingkey not in IN_PROG: IN_PROG.append(ratingkey) LOG.debug('Failed to find %s in the db', ratingkey) POOL.apply_async(task, args=(ratingkey, sessionkey))
def client_action(offset=None, sessionkey=None, action='jump'): # pragma: no cover """Seek the client to the offset. Args: offset(int): Default None sessionkey(int): So we made sure we control the correct client. Returns: None """ global JUMP_LIST LOG.info('Called client_action with %s %s %s %s', offset, to_time(offset), sessionkey, action) def proxy_on_fail(func): import plexapi @wraps(func) def inner(): try: func() except plexapi.exceptions.BadRequest: try: LOG.info( 'Failed to reach the client directly, trying via server.' ) correct_client.proxyThroughServer() func() except: # pragma: no cover correct_client.proxyThroughServer(value=False) raise if offset == -1: return conf_clients = CONFIG.get('general', {}).get('clients', []) conf_users = CONFIG.get('general', {}).get('users', []) correct_client = None clients = PMS.clients() for media in PMS.sessions(): # Find the client.. This client does not have the correct address # or 'protocolCapabilities' so we have to get the correct one. # or we can proxy thru the server.. if sessionkey and int(sessionkey) == media.sessionKey: client = media.players[0] user = media.usernames[0] LOG.info('client %s %s', client.title, (media.viewOffset / 1000)) # Check that this client is allowed. if conf_clients and client.title not in conf_clients: LOG.info('Client %s is not whitelisted', client.title) return # Check that this user is allowed. if conf_users and user not in conf_users: LOG.info('User %s is not whitelisted', user) return # To stop processing. from func task if we have used to much time.. # This will not work if/when credits etc are added. Need a better way. # if offset <= media.viewOffset / 1000: # LOG.debug('Didnt jump because of offset') # return for c in clients: LOG.info('%s %s' % (c.machineIdentifier, client.machineIdentifier)) # So we got the correct client.. if c.machineIdentifier == client.machineIdentifier: # Plex web sometimes add loopback.. if '127.0.0.1' in c._baseurl: c._baseurl = c._baseurl.replace( '127.0.0.1', client.address) correct_client = c break if correct_client: try: LOG.info('Connectiong to %s', correct_client.title) correct_client.connect() except requests.exceptions.ConnectionError: LOG.exception('Cant connect to %s', client.title) return if action != 'stop': if ignore_ratingkey( media, CONFIG['general'].get('ignore_intro_ratingkeys')): LOG.info( 'Didnt send seek command this show, season or episode is ignored' ) return # PMP seems to be really picky about timeline calls, if we dont # it returns 406 errors after 90 sec. if correct_client.product == 'Plex Media Player': correct_client.sendCommand('timeline/poll', wait=0) proxy_on_fail(correct_client.seekTo(int(offset * 1000))) LOG.info('Jumped %s %s to %s %s', user, client.title, offset, media._prettyfilename()) else: if not ignore_ratingkey( media, CONFIG['general'].get('ignore_intro_ratingkeys')): proxy_on_fail(correct_client.stop()) # We might need to login on pms as the user.. # urs_pms = users_pms(PMS, user) # new_media = urs_pms.fetchItem(int(media.ratingkey)) # new_media.markWatched() # LOG.debug('Stopped playback on %s and marked %s as watched.', client.title, media._prettyfilename()) # Check if we just start the next ep instantly. if CONFIG['tv'].get( 'check_credits_start_next_ep') is True: nxt = find_next( media) # This is always false for movies. if nxt: LOG.info('Start playback on %s with %s', user, nxt._prettyfilename()) proxy_on_fail(correct_client.playMedia(nxt)) else: LOG.info('Didnt find the correct client.') # Some clients needs some time.. # time.sleep(0.2) # client.play() # JUMP_LIST.remove(sessionkey) # time.sleep(1) return
def process_to_db(media, theme=None, vid=None, start=None, end=None, ffmpeg_end=None, recap=None): """Process a plex media item to the db Args: media (Episode obj): theme: path to the theme. vid: path to the stripped wav of the media item. start (None, int): of theme. end (None, int): of theme. ffmpeg_end (None, int): What does ffmpeg think is the start of the ep. Returns: None """ global HT # This will download the theme and add it to # the hashtable if its missing if theme is None: if HT.has_theme(media, add_if_missing=False) is False: LOG.debug('downloading theme from process_to_db') theme = download_theme(media, HT) ff = -1 name = media._prettyfilename() LOG.debug('Started to process %s', name) if vid is None: vid = convert_and_trim(check_file_access(media), fs=11025, trim=600) # Find the start and the end of the theme in the video file. if end is None: start, end = get_offset_end(vid, HT) # Guess when the intro ended using blackframes and audio silence. if ffmpeg_end is None: ffmpeg_end = find_offset_ffmpeg(check_file_access(media)) # Check for recap. if recap is None: recap = has_recap(media, CONFIG.get('words', []), audio=vid) with session_scope() as se: try: se.query(Preprocessed).filter_by(ratingKey=media.ratingKey).one() except NoResultFound: p = Preprocessed(show_name=media.grandparentTitle, ep_title=media.title, theme_end=end, theme_start=start, theme_start_str=to_time(start), theme_end_str=to_time(end), ffmpeg_end=ffmpeg_end, ffmpeg_end_str=to_time(ffmpeg_end), duration=media.duration, ratingKey=media.ratingKey, grandparentRatingKey=media.grandparentRatingKey, prettyname=media._prettyfilename(), updatedAt=media.updatedAt, has_recap=recap) se.add(p) LOG.debug('Added %s to media.db', name)
from multiprocessing.dummy import ThreadPool as Pool import click import requests from sqlalchemy.orm.exc import NoResultFound from bw_plex import FP_HASHES, CONFIG, THEMES, LOG, INI_FILE from bw_plex.config import read_or_make from bw_plex.credits import find_credits from bw_plex.db import session_scope, Processed from bw_plex.misc import (analyzer, convert_and_trim, choose, find_next, find_offset_ffmpeg, get_offset_end, get_pms, get_hashtable, has_recap, to_sec, to_time, download_theme, ignore_ratingkey) POOL = Pool(int(CONFIG.get('thread_pool_number', 10))) PMS = None IN_PROG = [] JUMP_LIST = [] SHOWS = {} HT = None is_64bit = struct.calcsize('P') * 8 if not is_64bit: LOG.info('You not using a python 64 bit version.') def log_exception(func): @wraps(func) def inner(*args, **kwargs): try:
def process_to_db(media, theme=None, vid=None, start=None, end=None, ffmpeg_end=None, recap=None, credits_start=None, credits_end=None): """Process a plex media item to the db Args: media (Episode obj): theme: path to the theme. vid: path to the stripped wav of the media item. start (None, int): of theme. end (None, int): of theme. ffmpeg_end (None, int): What does ffmpeg think is the start of the ep. recap(None, bool): If this how has a recap or not credits_start(None, int): The offset (in sec) the credits text starts credits_end(None, int): The offset (in sec) the credits text ends Returns: None """ global HT add_images = False # Disable for now. # if media.TYPE == 'movie': # return # This will download the theme and add it to # the hashtable if its missing if media.TYPE == 'episode' and theme is None: if HT.has_theme(media, add_if_missing=False) is False: LOG.debug('downloading theme from process_to_db') theme = download_theme(media, HT) name = media._prettyfilename() LOG.debug('Started to process %s', name) if vid is None and media.TYPE == 'episode': vid = convert_and_trim(check_file_access(media), fs=11025, trim=CONFIG['tv'].get('check_for_theme_sec', 600)) # Find the start and the end of the theme in the episode file. if end is None and media.TYPE == 'episode': start, end = get_offset_end(vid, HT) # Guess when the intro ended using blackframes and audio silence. if ffmpeg_end is None: if media.TYPE == 'episode': trim = CONFIG['tv'].get('check_intro_ffmpeg_sec') else: trim = CONFIG['movie'].get('check_intro_ffmpeg_sec') ffmpeg_end = find_offset_ffmpeg(check_file_access(media), trim=trim) # Check for recap. if recap is None: recap = has_recap(media, CONFIG['tv'].get('words', []), audio=vid) if (media.TYPE == 'episode' and CONFIG['tv'].get('check_credits') is True and credits_start is None and credits_end is None): dur = media.duration / 1000 - CONFIG['tv'].get('check_credits_sec', 120) credits_start, credits_end = find_credits(check_file_access(media), offset=dur, check=-1) elif (media.TYPE == 'movie' and CONFIG['movie'].get('check_credits') is True and credits_start is None and credits_end is None): dur = media.duration / 1000 - CONFIG['movie'].get( 'check_credits_sec', 600) credits_start, credits_end = find_credits(check_file_access(media), offset=dur, check=-1) else: # We dont want to find the credits. credits_start = -1 credits_end = -1 # We assume this is kinda right, # double check this # TODO location = list(i.file for i in media.iterParts() if i)[0] with session_scope() as se: try: se.query(Processed).filter_by(ratingKey=media.ratingKey).one() except NoResultFound: if media.TYPE == 'episode': p = Processed(show_name=media.grandparentTitle, title=media.title, type=media.TYPE, theme_end=end, theme_start=start, theme_start_str=to_time(start), theme_end_str=to_time(end), ffmpeg_end=ffmpeg_end, ffmpeg_end_str=to_time(ffmpeg_end), credits_start=credits_start, credits_start_str=to_time(credits_start), credits_end=credits_end, credits_end_str=to_time(credits_end), duration=media.duration, ratingKey=media.ratingKey, grandparentRatingKey=media.grandparentRatingKey, prettyname=media._prettyfilename(), updatedAt=media.updatedAt, has_recap=recap, location=location) elif media.TYPE == 'movie': p = Processed(title=media.title, type=media.TYPE, ffmpeg_end=ffmpeg_end, ffmpeg_end_str=to_time(ffmpeg_end), credits_start=credits_start, credits_start_str=to_time(credits_start), credits_end=credits_end, credits_end_str=to_time(credits_end), duration=media.duration, ratingKey=media.ratingKey, prettyname=media._prettyfilename(), updatedAt=media.updatedAt, location=location) se.add(p) LOG.debug('Added %s to media.db', name) if media.TYPE == 'movie' and CONFIG['movie']['create_edl']: edl.write_edl( location, edl.db_to_edl(p, type=CONFIG['movie']['edl_action_type'])) elif media.TYPE == 'episode' and CONFIG['tv']['create_edl']: edl.write_edl( location, edl.db_to_edl(p, type=CONFIG['tv']['edl_action_type'])) if media.TYPE == 'episode': try: # since it will check every ep if will download hashes from every ep. We might get # away with just checking 2-4 eps. Should this be a config option? # we could checkfor grandparentkey and see if we have the required amount n = se.query(Images).filter_by(ratingKey=media.ratingKey).one() except NoResultFound: add_images = True if media.TYPE == 'episode' and CONFIG.get('hashing').get( 'check_frames') and add_images: img_hashes = [] for imghash, frame, pos in hash_file(check_file_access( media)): # Add config option of get frames ever n. img = Images(ratingKey=media.ratingKey, hex=str(imghash), hash=imghash.hash.tostring(), grandparentRatingKey=media.grandparentRatingKey, offset=pos, time=to_time(pos)) img_hashes.append(img) with session_scope() as ssee: ssee.add_all(img_hashes)