def download_subtitle(episode): import srt episode.reload() LOG.debug('Downloading subtitle from PMS') pms = episode._server to_dl = [] all_subs = [] for part in episode.iterParts(): if part.subtitleStreams(): for sub in part.subtitleStreams(): if sub.key and sub.codec == 'srt': to_dl.append( pms.url('%s?download=1' % sub.key, includeToken=True)) for dl_url in to_dl: r = requests.get(dl_url) r.raise_for_status() if r: try: a_sub = list(srt.parse(r.text)) all_subs.append(a_sub) except ValueError: LOG.exception('Failed to parse subtitle') return all_subs
def download_subtitle(episode): episode.reload() LOG.debug('Downloading subtitle from PMS') pms = episode._server to_dl = [] all_subs = [] for part in episode.iterParts(): if part.subtitleStreams(): for sub in part.subtitleStreams(): if sub.key and sub.codec in FILE_EXTENSION_TO_FORMAT_IDENTIFIER.values( ): to_dl.append( pms.url('%s?download=1' % sub.key, includeToken=True)) for dl_url in to_dl: r = episode._server._session.get(dl_url) r.raise_for_status() if r: try: subt = [ sub for sub in SSAFile.from_string(r.text, encoding=r.encoding) ] all_subs.append(subt) except (IOError, pysubs2.exceptions.UnknownFPSError, pysubs2.exceptions.UnknownFormatIdentifierError, pysubs2.exceptions.FormatAutodetectionError): LOG.exception('Failed to parse subtitle') return all_subs
def get_themes(self): d = defaultdict(list) for n in self.names: try: rk = os.path.basename(n).split('__')[1] d[int(rk)].append(n) except (IndexError, TypeError): LOG.exception('Some crap happend with', n) return d
def inner(*args, **kwargs): try: if kwargs: return func(*args, **kwargs) else: return func(*args) except: err = "There was an exception in " err += func.__name__ LOG.exception(err) raise
def convert_and_trim(afile, fs=8000, trim=None, theme=False, filename=None): tmp = tempfile.NamedTemporaryFile(mode='r+b', prefix='offset_', suffix='.wav') tmp_name = tmp.name tmp.close() tmp_name = "%s" % tmp_name if os.name == 'nt' and '://' not in afile: q_file = '"%s"' % afile else: q_file = afile if trim is None: cmd = [ 'ffmpeg', '-i', q_file, '-ac', '1', '-ar', str(fs), '-acodec', 'pcm_s16le', tmp_name ] else: cmd = [ 'ffmpeg', '-i', q_file, '-ac', '1', '-ar', str(fs), '-ss', '0', '-t', str(trim), '-acodec', 'pcm_s16le', tmp_name ] LOG.debug('calling ffmpeg with %s' % ' '.join(cmd)) if os.name == 'nt': cmd = '%s' % ' '.join(cmd) psox = subprocess.Popen(cmd, stderr=subprocess.PIPE) o, e = psox.communicate() if not psox.returncode == 0: # pragma: no cover LOG.exception(e) raise Exception("FFMpeg failed") # Check if we passed a url. if '://' in afile and filename: filename = filename + '.wav' afile = os.path.join(THEMES, filename) if theme: shutil.move(tmp_name, afile) LOG.debug('Done converted and moved %s to %s' % (afile, THEMES)) return afile else: LOG.debug('Done converting %s', tmp_name) return tmp_name
def create_edl_from_db(t, save_path): with session_scope() as se: db_items = se.query(Processed).all() for item in db_items: # Maybe remove this later? if save_path: loc = edl.create_edl_path( os.path.join(save_path, os.path.basename(item.location))) else: loc = item.location try: t = edl.write_edl(loc, edl.db_to_edl(item, edl.TYPES[t])) click.echo('Wrote %s' % t) except: LOG.exception('Failed to write edl.')
def task(item, sessionkey): """Main func for processing a episode. Args: item(str): a episode's ratingkey sessionkey(str): streams sessionkey Returns: None """ global HT media = PMS.fetchItem(int(item)) LOG.debug('Found %s', media._prettyfilename()) if media.TYPE not in ('episode', 'show', 'movie'): # pragma: no cover return if media.TYPE == 'episode': LOG.debug('Download the first 10 minutes of %s as .wav', media._prettyfilename()) vid = convert_and_trim(check_file_access(media), fs=11025, trim=CONFIG['tv'].get('check_for_theme_sec', 600)) process_to_db(media, vid=vid) try: os.remove(vid) LOG.debug('Deleted %s', vid) except IOError: # pragma: no cover LOG.exception('Failed to delete %s', vid) elif media.TYPE == 'movie': process_to_db(media) try: IN_PROG.remove(item) except ValueError: # pragma: no cover LOG.debug('Failed to remove %s from IN_PROG', item) nxt = find_next(media) if nxt: process_to_db(nxt)
def convert_and_trim(afile, fs=8000, trim=None, theme=False): tmp = tempfile.NamedTemporaryFile(mode='r+b', prefix='offset_', suffix='.wav') tmp_name = tmp.name tmp.close() if trim is None: cmd = [ 'ffmpeg', '-i', afile, '-ac', '1', '-ar', str(fs), '-acodec', 'pcm_s16le', tmp_name ] else: cmd = [ 'ffmpeg', '-i', afile, '-ac', '1', '-ar', str(fs), '-ss', '0', '-t', str(trim), '-acodec', 'pcm_s16le', tmp_name ] LOG.debug('calling ffmpeg with %s' % ' '.join(cmd)) psox = subprocess.Popen(cmd, stderr=subprocess.PIPE) o, e = psox.communicate() if not psox.returncode == 0: LOG.exception(e) raise Exception("FFMpeg failed") if theme: shutil.move(tmp_name, afile) LOG.debug('Done converted and moved %s to %s' % (afile, THEMES)) return afile else: LOG.debug('Done converting %s', tmp_name) return tmp_name
def search_for_theme_youtube(name, rk=1337, save_path=None, url=None): import youtube_dl LOG.debug('Searching youtube for name %s rk %s save_path %s url %s ' % (name, rk, save_path, url)) if isinstance(name, tuple): name, rk, save_path = name if save_path is None: save_path = os.getcwd() if url and 'youtube' not in url: return [] fp = os.path.join(save_path, '%s__%s__%s' % (name, rk, int(time.time()))) fp = get_valid_filename(fp) # Youtuble dl requires the template to be unicode. t = u'%s' % fp ydl_opts = { 'quiet': True, 'continuedl': True, 'external_downloader': 'ffmpeg', #'verbose': True, 'outtmpl': t + u'.%(ext)s', 'default_search': 'ytsearch', # So we select "best" here since this does not get throttled by # youtube. Should it be a config option for ppl with data caps? # Possible format could be bestaudio for those poor fuckers.. 'format': 'best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav', 'preferredquality': '192', }], 'logger': LOG, } # https://github.com/rg3/youtube-dl/issues/6923 # ydl_opts['external_downloader'] = 'aria2c' # ydl_opts['external_downloader_args'] = []#['-x', '8', '-s', '8', '-k', '256k'] ydl = youtube_dl.YoutubeDL(ydl_opts) def nothing(*args, **kwargs): pass ydl.to_screen = nothing name = name.replace(':', '') with ydl: try: if url: ydl.download([url]) else: ydl.download([name + ' theme song']) return t + '.wav' except: # pragma: no cover LOG.exception('Failed to download theme song %s' % name) LOG.debug('Done downloading theme for %s', name) return t + '.wav'
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 find_credits(path, offset=0, fps=None, duration=None, check=7, step=1, frame_range=True): """Find the start/end of the credits and end in a videofile. This only check frames so if there is any silence in the video this is simply skipped as opencv only handles videofiles. use frame_range to so we only check frames every 1 sec. # TODO just ffmepg to check for silence so we calculate the correct time? :( Args: path (str): path to the videofile offset(int): If given we should start from this one. fps(float?): fps of the video file duration(None, int): Duration of the vfile in seconds. check(int): Stop after n frames with text, set a insane high number to check all. end is not correct without this! step(int): only use every n frame frame_range(bool). default true, precalc the frames and only check thous frames. Returns: 1, 2 """ # LOG.debug('%r %r %r %r %r %r %r', path, offset, fps, duration, check, step, frame_range) if cv2 is None: return frames = [] start = -1 end = -1 LOG.debug('Trying to find the credits for %s', path) try: if fps is None: # we can just grab the fps from plex. cap = cv2.VideoCapture(path) fps = cap.get(cv2.CAP_PROP_FPS) cap.release() for i, (frame, millisec) in enumerate(video_frame_by_frame(path, offset=offset, step=step, frame_range=frame_range)): # LOG.debug('progress %s', millisec / 1000) if frame is not None: recs = locate_text(frame, debug=False) if recs: frames.append(millisec) if check != -1 and len(frames) >= check: break if frames: LOG.debug(frames) start = min(frames) / 1000 end = max(frames) / 1000 LOG.debug('credits_start %s, credits_end %s', start, end) except: # pragma: no cover # We just want to log the exception not halt the entire process to db. LOG.exception('There was a error in find_credits') return start, end
def find_credits(path, offset=0, fps=None, duration=None, check=7, step=1, frame_range=True, debug=False, method='east'): """Find the start/end of the credits and end in a videofile. This only check frames so if there is any silence in the video this is simply skipped as opencv only handles videofiles. use frame_range to so we only check frames every 1 sec. # TODO just ffmepg to check for silence so we calculate the correct time? :( Args: path (str): path to the videofile offset(int): If given we should start from this one. fps(float?): fps of the video file duration(None, int): Duration of the vfile in seconds. check(int): Stop after n frames with text, set a insane high number to check all. end is not correct without this! step(int): only use every n frame frame_range(bool). default true, precalc the frames and only check thous frames. debug(bool): Disable the images. method(str): east is better but slower. Returns: 1, 2 """ # LOG.debug('%r %r %r %r %r %r %r', path, offset, fps, duration, check, step, frame_range) if cv2 is None: return frames = [] start = -1 end = -1 LOG.debug('Trying to find the credits for %s', path) if method == 'east': func = locate_text_east else: func = locate_text try: if fps is None: # we can just grab the fps from plex. cap = cv2.VideoCapture(path) fps = cap.get(cv2.CAP_PROP_FPS) cap.release() for _, (frame, millisec) in enumerate(video_frame_by_frame(path, offset=offset, step=step, frame_range=frame_range)): try: # LOG.debug('progress %s', millisec / 1000) if frame is not None: # recs = locate_text(frame, debug=True) recs = func(frame, debug=debug) len_recs = len(recs) # If we get 1 match we should verify. # now this is pretty harsh but we really # don't want false positives. if len_recs == 0: continue elif len_recs == 1: t = extract_text(frame) if t: frames.append(millisec) else: frames.append(millisec) # check for motion here? if check != -1 and len(frames) >= check: break except DEBUG_STOP: break if hasattr(cv2, 'destroyAllWindows'): cv2.destroyAllWindows() if frames: start = min(frames) / 1000 end = max(frames) / 1000 LOG.debug('credits_start %s, credits_end %s', start, end) except: # pragma: no cover # We just want to log the exception not halt the entire process to db. LOG.exception('There was a error in find_credits') return start, end