def test_movie_list_movies(self, api_client): # Get non existent list rsp = api_client.get('/movie_list/1/movies/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code identifier = {'imdb_id': 'tt1234567'} movie_data = {'title': 'title', 'original_url': 'http://test.com', 'movie_identifiers': [identifier]} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code # Get movies from list rsp = api_client.get('/movie_list/1/movies/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code returned_identifier = json.loads(rsp.data)['movies'][0]['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifier.items()[0]
def test_change_password(self, execute_task, api_client, schema_match): weak_password = {'password': '******'} medium_password = {'password': '******'} strong_password = {'password': '******'} rsp = api_client.json_put('/user/', data=json.dumps(weak_password)) assert rsp.status_code == 400 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors rsp = api_client.json_put('/user/', data=json.dumps(medium_password)) assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors rsp = api_client.json_put('/user/', data=json.dumps(strong_password)) assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def test_movie_list_movies(self, api_client, schema_match): payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code movie_data = {'movie_name': 'title'} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code movie = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, movie) assert not errors # Get movies from list rsp = api_client.get('/movie_list/1/movies/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.return_movies, data) assert not errors assert data[0] == movie # Get movies from non-existent list rsp = api_client.get('/movie_list/10/movies/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def test_schedules_id_put(self, mocked_save_config, api_client, schema_match): # Get schedules to get their IDs rsp = api_client.get('/schedules/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.schedules_list, data) assert not errors schedule_id = data[0]['id'] payload = {'tasks': ['test2', 'test3'], 'interval': {'minutes': 10}} rsp = api_client.json_put('/schedules/{}/'.format(schedule_id), data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.schedule_object, data) assert not errors assert mocked_save_config.called del data['id'] assert data == payload rsp = api_client.json_put('/schedules/1011/', data=json.dumps(payload)) assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def test_movie_list_movies_with_identifiers(self, api_client, schema_match): payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code identifier = {'imdb_id': 'tt1234567'} movie_data = {'movie_name': 'title', 'movie_identifiers': [identifier]} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors # Get movies from list rsp = api_client.get('/movie_list/1/movies/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.return_movies, data) assert not errors returned_identifier = data['movies'][0]['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifier.items()[0]
def send_push(self, task, api_key, title, body, url=None, destination=None, destination_type=None): if url: push_type = 'link' else: push_type = 'note' data = {'type': push_type, 'title': title, 'body': body} if url: data['url'] = url if destination: data[destination_type] = destination # Check for test mode if task.options.test: log.info('Test mode. Pushbullet notification would be:') log.info(' API Key: %s' % api_key) log.info(' Type: %s' % push_type) log.info(' Title: %s' % title) log.info(' Body: %s' % body) if destination: log.info(' Destination: %s (%s)' % (destination, destination_type)) if url: log.info(' URL: %s' % url) log.info(' Raw Data: %s' % json.dumps(data)) # Test mode. Skip remainder. return # Make the request headers = { 'Authorization': b'Basic %s' % base64.b64encode(api_key.encode('ascii')), 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Flexget' } response = task.requests.post(pushbullet_url, headers=headers, data=json.dumps(data), raise_status=False) # Check if it succeeded request_status = response.status_code # error codes and messages from Pushbullet API if request_status == 200: log.debug('Pushbullet notification sent') elif request_status == 500: log.warning('Pushbullet notification failed, Pushbullet API having issues') # TODO: Implement retrying. API requests 5 seconds between retries. elif request_status >= 400: error = 'Unknown error' if response.content: try: error = response.json()['error']['message'] except ValueError as e: error = 'Unknown Error (Invalid JSON returned): %s' % e log.error('Pushbullet API error: %s' % error) else: log.error('Unknown error when sending Pushbullet notification')
def test_movie_list_movie(self, api_client, schema_match): payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code identifier = {'imdb_id': 'tt1234567'} movie_data = {'movie_name': 'title', 'movie_identifiers': [identifier]} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code # Get specific movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors returned_identifier = data['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifier.items()[0] identifiers = [{'trakt_movie_id': '12345'}] # Change specific movie from list rsp = api_client.json_put('/movie_list/1/movies/1/', data=json.dumps(identifiers)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors returned_identifier = data['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifiers[0].items() # Delete specific movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(empty_response, data) assert not errors # Get non existent movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code # Delete non existent movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code
def on_task_output(self, task, config): """Add accepted episodes and/or movies to uoccin's collection""" series = {} movies = {} for entry in task.accepted: if all(field in entry for field in ['tvdb_id', 'series_season', 'series_episode']): eid = '%s.S%02dE%02d' % (entry['tvdb_id'], entry['series_season'], entry['series_episode']) if not eid in series: # we can have more than one (different release/quality) series[eid] = [] if self.acquire and 'subtitles' in entry: subs = series[eid] series[eid] = list(set(subs + entry['subtitles'])) elif all(field in entry for field in ['imdb_id', 'movie_name']): eid = entry['imdb_id'] if not eid in movies: # we can have more than one (different release/quality) movies[eid] = {'name': entry.get('imdb_name', entry['movie_name'])} if self.acquire and 'subtitles' in entry: subs = movies[eid]['subtitles'] if 'subtitles' in movies[eid] else [] movies[eid]['subtitles'] = list(set(subs + entry['subtitles'])) if series: dest = os.path.join(config, 'series.collected.json') data = {} if os.path.exists(dest): with open(dest, 'r') as f: data = json.load(f) for eid in series: if self.acquire: log.info('adding/updating episode %s to Uoccin collection' % eid) data[eid] = series[eid] elif eid in data: log.info('removing episode %s from Uoccin collection' % eid) data.pop(eid) text = json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) with open(dest, 'w') as f: f.write(text) log.debug('Uoccin episodes collection updated') if movies: dest = os.path.join(config, 'movies.collected.json') data = {} if os.path.exists(dest): with open(dest, 'r') as f: data = json.load(f) for eid in movies: if self.acquire: log.info('adding/updating movie %s to Uoccin collection' % eid) data[eid] = movies[eid] elif eid in data: log.info('removing movie %s from Uoccin collection' % eid) data.pop(eid) text = json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) with open(dest, 'w') as f: f.write(text) log.debug('Uoccin movies collection updated')
def test_change_password(self, execute_task, api_client): weak_password = {'password': '******'} medium_password = {'password': '******'} strong_password = {'password': '******'} rsp = api_client.json_put('/user/', data=json.dumps(weak_password)) assert rsp.status_code == 500 rsp = api_client.json_put('/user/', data=json.dumps(medium_password)) assert rsp.status_code == 200 rsp = api_client.json_put('/user/', data=json.dumps(strong_password)) assert rsp.status_code == 200
def test_movie_list_list(self, api_client, schema_match): # No params rsp = api_client.get('/movie_list/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.return_lists, data) assert not errors assert data == [] # Named param rsp = api_client.get('/movie_list/?name=name') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.return_lists, data) assert not errors payload = {'name': 'test'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.list_object, data) assert not errors values = { 'name': 'test', 'id': 1 } for field, value in values.items(): assert data.get(field) == value rsp = api_client.get('/movie_list/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.return_lists, data) assert not errors for field, value in values.items(): assert data[0].get(field) == value # Try to Create existing list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 409, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def on_task_input(self, task, config): # Don't edit the config, or it won't pass validation on rerun url_params = config.copy() if 'movies' in config and 'series' in config: raise PluginError('Cannot use both series list and movies list in the same task.') if 'movies' in config: url_params['data_type'] = 'movies' url_params['list_type'] = config['movies'] map = self.movie_map elif 'series' in config: url_params['data_type'] = 'shows' url_params['list_type'] = config['series'] map = self.series_map elif 'custom' in config: url_params['data_type'] = 'custom' # Do some translation from visible list name to prepare for use in url list_name = config['custom'].lower() # These characters are just stripped in the url for char in '!@#$%^*()[]{}/=?+\\|-_': list_name = list_name.replace(char, '') # These characters get replaced list_name = list_name.replace('&', 'and') list_name = list_name.replace(' ', '-') url_params['list_type'] = list_name # Map type is per item in custom lists else: raise PluginError('Must define movie or series lists to retrieve from trakt.') url = 'http://api.trakt.tv/user/' auth = None if url_params['data_type'] == 'custom': url += 'list.json/%(api_key)s/%(username)s/%(list_type)s' elif url_params['list_type'] == 'watchlist': url += 'watchlist/%(data_type)s.json/%(api_key)s/%(username)s' else: url += 'library/%(data_type)s/%(list_type)s.json/%(api_key)s/%(username)s' url = url % url_params if 'password' in config: auth = {'username': config['username'], 'password': hashlib.sha1(config['password']).hexdigest()} entries = [] log.verbose('Retrieving list %s %s...' % (url_params['data_type'], url_params['list_type'])) result = task.requests.get(url, data=json.dumps(auth)) try: data = task.requests.post(url, data=json.dumps(auth)).json except RequestException, e: raise PluginError('Could not retrieve list from trakt (%s)' % e.message)
def test_pending_api_put_entry(self, api_client, schema_match): e1 = Entry(title='test.title1', url='http://bla.com') e2 = Entry(title='test.title1', url='http://bla.com') with Session() as session: pe1 = PendingEntry('test_task', e1) pe2 = PendingEntry('test_task', e2) session.bulk_save_objects([pe1, pe2]) payload = {'operation': 'approve'} rsp = api_client.json_put('/pending/1/', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_entry_object, data) assert not errors assert data['approved'] is True rsp = api_client.json_put('/pending/1/', data=json.dumps(payload)) assert rsp.status_code == 400 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors payload = {'operation': 'reject'} rsp = api_client.json_put('/pending/1/', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_entry_object, data) assert not errors assert data['approved'] is False rsp = api_client.json_put('/pending/1/', data=json.dumps(payload)) assert rsp.status_code == 400 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def send_post(self, task, webhook_url, text, channel, username, icon_emoji): data = {'text': text} if channel: data['channel'] = channel if username: data['username'] = username if icon_emoji: data['icon_emoji'] = icon_emoji if task.options.test: log.info('Test mode. Slack notification would be:') log.info(' Webhook URL: {0}'.format(webhook_url)) log.info(' Text: {0}'.format(text)) if channel: log.info(' Channel: {0}'.format(channel)) if username: log.info(' Username: {0}'.format(username)) if icon_emoji: log.info(' Icon Emoji: :{0}:'.format(icon_emoji)) log.info(' Raw POST Data: {0}'.format(json.dumps(data))) # Early return (test mode) return headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Flexget' } try: response = task.requests.post(webhook_url, headers=headers, data=json.dumps(data), raise_status=False) except ConnectionError as e: log.error('Unable to connect to Slack API: {0}'.format(e.message)) return response_code = response.status_code if response_code == 200: log.debug('Slack notification sent') elif response_code == 500: log.warning('Slack notification failed: server problem') elif response_code >= 400: log.error('Slack API error {0}: {1}'.format(response_code, response.content)) else: log.error('Unknown error when sending Slack notification: {0}'.format(response_code))
def upgrade(ver, session): if ver is None: # Upgrade to version 0 was a failed attempt at cleaning bad entries from our table, better attempt in ver 1 ver = 1 if ver == 1: table = table_schema('delay', session) table_add_column(table, 'json', Unicode, session) # Make sure we get the new schema with the added column table = table_schema('delay', session) failures = 0 for row in session.execute(select([table.c.id, table.c.entry])): try: p = pickle.loads(row['entry']) session.execute( table.update() .where(table.c.id == row['id']) .values(json=json.dumps(p, encode_datetime=True)) ) except (KeyError, ImportError): failures += 1 if failures > 0: log.error( 'Error upgrading %s pickle objects. Some delay information has been lost.' % failures ) ver = 2 return ver
def get_session(username=None, password=None): """Creates a requests session which is authenticated to trakt.""" session = Session() session.headers = { 'Content-Type': 'application/json', 'trakt-api-version': 2, 'trakt-api-key': API_KEY } if username: session.headers['trakt-user-login'] = username if username and password: auth = {'login': username, 'password': password} try: r = session.post(urljoin(API_URL, 'auth/login'), data=json.dumps(auth)) except Timeout: # requests.exceptions.Timeout raise plugin.PluginError('Authentication timed out to trakt') except RequestException as e: if hasattr(e, 'response') and e.response.status_code in [401, 403]: raise plugin.PluginError('Authentication to trakt failed, check your username/password: %s' % e.args[0]) else: raise plugin.PluginError('Authentication to trakt failed: %s' % e.args[0]) try: session.headers['trakt-user-token'] = r.json()['token'] except (ValueError, KeyError): raise plugin.PluginError('Got unexpected response content while authorizing to trakt: %s' % r.text) return session
def upgrade(ver, session): if ver is None: # Upgrade to version 0 was a failed attempt at cleaning bad entries from our table, better attempt in ver 1 ver = 0 if ver == 0: # Remove any values that are not loadable. table = table_schema('simple_persistence', session) for row in session.execute(select([table.c.id, table.c.plugin, table.c.key, table.c.value])): try: pickle.loads(row['value']) except Exception as e: log.warning('Couldn\'t load %s:%s removing from db: %s' % (row['plugin'], row['key'], e)) session.execute(table.delete().where(table.c.id == row['id'])) ver = 1 if ver == 1: log.info('Creating index on simple_persistence table.') create_index('simple_persistence', session, 'feed', 'plugin', 'key') ver = 2 if ver == 2 or ver == 3: table = table_schema('simple_persistence', session) table_add_column(table, 'json', Unicode, session) # Make sure we get the new schema with the added column table = table_schema('simple_persistence', session) for row in session.execute(select([table.c.id, table.c.value])): try: p = pickle.loads(row['value']) session.execute(table.update().where(table.c.id == row['id']).values( json=json.dumps(p, encode_datetime=True))) except KeyError as e: log.error('Unable error upgrading simple_persistence pickle object due to %s' % str(e)) ver = 4 return ver
def test_seen_delete_all(self, mock_seen_search, api_client): session = Session() entry_list = session.query(SeenEntry).join(SeenField) mock_seen_search.return_value = entry_list # No params rsp = api_client.delete('/seen/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code fields = { 'url': 'http://test.com/file.torrent', 'title': 'Test.Title', 'torrent_hash_id': 'dsfgsdfg34tq34tq34t' } entry = { 'local': False, 'reason': 'test_reason', 'task': 'test_task', 'title': 'Test.Title', 'fields': fields } rsp = api_client.json_post('/seen/', data=json.dumps(entry)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code # With value rsp = api_client.delete('/seen/?value=Test.Title') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code assert mock_seen_search.call_count == 2, 'Should have 2 calls, is actually %s' % mock_seen_search.call_count
def upgrade(ver, session): if ver is None: # Make sure there is no data we can't load in the backlog table backlog_table = table_schema('backlog', session) try: for item in session.query('entry').select_from(backlog_table).all(): pickle.loads(item.entry) except (ImportError, TypeError): # If there were problems, we can drop the data. log.info('Backlog table contains unloadable data, clearing old data.') session.execute(backlog_table.delete()) ver = 0 if ver == 0: backlog_table = table_schema('backlog', session) log.info('Creating index on backlog table.') Index('ix_backlog_feed_expire', backlog_table.c.feed, backlog_table.c.expire).create(bind=session.bind) ver = 1 if ver == 1: table = table_schema('backlog', session) table_add_column(table, 'json', Unicode, session) # Make sure we get the new schema with the added column table = table_schema('backlog', session) for row in session.execute(select([table.c.id, table.c.entry])): try: p = pickle.loads(row['entry']) session.execute(table.update().where(table.c.id == row['id']).values( json=json.dumps(p, encode_datetime=True))) except KeyError as e: log.error('Unable error upgrading backlog pickle object due to %s' % str(e)) ver = 2 return ver
def search(self, task, entry, config): api_key = config searches = entry.get('search_strings', [entry['title']]) if 'series_name' in entry: search = {'category': 'Episode'} if 'tvdb_id' in entry: search['tvdb'] = entry['tvdb_id'] elif 'tvrage_id' in entry: search['tvrage'] = entry['tvrage_id'] else: search['series'] = entry['series_name'] if 'series_id' in entry: # BTN wants an ep style identifier even for sequence shows if entry.get('series_id_type') == 'sequence': search['name'] = 'S01E%02d' % entry['series_id'] else: search['name'] = entry['series_id'] + '%' # added wildcard search for better results. searches = [search] # If searching by series name ending in a parenthetical, try again without it if there are no results. if search.get('series') and search['series'].endswith(')'): match = re.match('(.+)\([^\(\)]+\)$', search['series']) if match: searches.append(dict(search, series=match.group(1).strip())) results = set() for search in searches: data = json.dumps({'method': 'getTorrents', 'params': [api_key, search], 'id': 1}) try: r = task.requests.post('http://api.btnapps.net/', data=data, headers={'Content-type': 'application/json'}) except requests.RequestException as e: log.error('Error searching btn: %s' % e) continue content = r.json() if not content or not content['result']: log.debug('No results from btn') if content and content.get('error'): log.error('Error searching btn: %s' % content['error'].get('message', content['error'])) continue if 'torrents' in content['result']: for item in content['result']['torrents'].itervalues(): entry = Entry() entry['title'] = item['ReleaseName'] entry['title'] += ' '.join(['', item['Resolution'], item['Source'], item['Codec']]) entry['url'] = item['DownloadURL'] entry['torrent_seeds'] = int(item['Seeders']) entry['torrent_leeches'] = int(item['Leechers']) entry['torrent_info_hash'] = item['InfoHash'] entry['search_sort'] = torrent_availability(entry['torrent_seeds'], entry['torrent_leeches']) if item['TvdbID'] and int(item['TvdbID']): entry['tvdb_id'] = int(item['TvdbID']) if item['TvrageID'] and int(item['TvrageID']): entry['tvrage_id'] = int(item['TvrageID']) results.add(entry) # Don't continue searching if this search yielded results break return results
def setter(self, entry): if isinstance(entry, Entry) or isinstance(entry, dict): setattr( self, name, unicode(json.dumps(only_builtins(dict(entry)), encode_datetime=True)) ) else: raise TypeError('%r is not of type Entry or dict.' % type(entry))
def search(self, entry, config): api_key = config searches = entry.get('search_strings', [entry['title']]) if 'series_name' in entry: search = {'series': entry['series_name']} if 'series_id' in entry: search['name'] = entry['series_id'] searches = [search] results = [] for search in searches: data = json.dumps({'method': 'getTorrents', 'params': [api_key, search], 'id': 1}) try: r = session.post('http://api.btnapps.net/', data=data, headers={'Content-type': 'application/json'}) except requests.RequestException as e: log.error('Error searching btn: %s' % e) continue content = r.json() if content['result']['results']: for item in content['result']['torrents'].itervalues(): if item['Category'] != 'Episode': continue entry = Entry() entry['title'] = item['ReleaseName'] entry['url'] = item['DownloadURL'] entry['torrent_seeds'] = int(item['Seeders']) entry['torrent_leeches'] = int(item['Leechers']) entry['torrent_info_hash'] = item['InfoHash'] entry['search_sort'] = torrent_availability(entry['torrent_seeds'], entry['torrent_leeches']) if item['TvdbID']: entry['tvdb_id'] = int(item['TvdbID']) results.append(entry) return results
def test_movie_list_movie(self, api_client): payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code identifier = {'imdb_id': 'tt1234567'} movie_data = {'title': 'title', 'original_url': 'http://test.com', 'movie_identifiers': [identifier]} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code # Get movies from list rsp = api_client.get('/movie_list/1/movies/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))['movies'][0]['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifier.items()[0] # Get specific movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifier.items()[0] identifiers = [{'trakt_movie_id': '12345'}] # Change specific movie from list rsp = api_client.json_put('/movie_list/1/movies/1/', data=json.dumps(identifiers)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier['id_value'] == identifiers[0].items() # Delete specific movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code # Get non existent movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code # Delete non existent movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code
def process_notifications(self, task, entries, config): for entry in entries: if task.manager.options.test: log.info("Would send RapidPush notification about: %s", entry['title']) continue log.info("Send RapidPush notification about: %s", entry['title']) apikey = entry.get('apikey', config['apikey']) if isinstance(apikey, list): apikey = ','.join(apikey) priority = entry.get('priority', config['priority']) category = entry.get('category', config['category']) try: category = entry.render(category) except RenderError as e: log.error('Error setting RapidPush category: %s' % e) title = config['title'] try: title = entry.render(title) except RenderError as e: log.error('Error setting RapidPush title: %s' % e) message = config['message'] try: message = entry.render(message) except RenderError as e: log.error('Error setting RapidPush message: %s' % e) group = entry.get('group', config['group']) try: group = entry.render(group) except RenderError as e: log.error('Error setting RapidPush group: %s' % e) # Send the request data_string = json.dumps({ 'title': title, 'message': message, 'priority': priority, 'category': category, 'group': group}) data = {'apikey': apikey, 'command': 'notify', 'data': data_string} response = task.requests.post(url, headers=headers, data=data, raise_status=False) json_data = response.json() if json_data.has_key('code'): if json_data['code'] == 200: log.debug("RapidPush message sent") else: log.error(json_data['desc'] + " (" + str(json_data['code']) + ")") else: for item in json_data: if json_data[item]['code'] == 200: log.debug(item + ": RapidPush message sent") else: log.error(item + ": " + json_data[item]['desc'] + " (" + str(json_data[item]['code']) + ")")
def test_adding_same_movie(self, api_client, schema_match): payload = {'name': 'test'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.list_object, data) assert not errors movie = {'movie_name': 'test movie', 'movie_year': 2000} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors # Try to add it again rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie)) assert rsp.status_code == 409, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors movie_2 = copy.deepcopy(movie) movie_2['movie_year'] = 1999 # Add same movie name, different year rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_2)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors movie_3 = copy.deepcopy(movie) del movie_3['movie_year'] # Add same movie, no year rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_3)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors
def test_schedules_id_put(self, api_client, schema_match): payload = {'tasks': ['test2', 'test3'], 'interval': {'minutes': 10}} rsp = api_client.json_put('/schedules/1/', data=json.dumps(payload)) assert rsp.status_code == 409, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def check_auth(): if ( task.requests.post( "http://api.trakt.tv/account/test/" + config["api_key"], data=json.dumps(auth), raise_status=False ).status_code != 200 ): raise PluginError("Authentication to trakt failed.")
def test_movie_list_movie(self, api_client): payload = {"name": "name"} # Create list rsp = api_client.json_post("/movie_list/", data=json.dumps(payload)) assert rsp.status_code == 201, "Response code is %s" % rsp.status_code identifier = {"imdb_id": "tt1234567"} movie_data = {"title": "title", "original_url": "http://test.com", "movie_identifiers": [identifier]} # Add movie to list rsp = api_client.json_post("/movie_list/1/movies/", data=json.dumps(movie_data)) assert rsp.status_code == 201, "Response code is %s" % rsp.status_code # Get movies from list rsp = api_client.get("/movie_list/1/movies/") assert rsp.status_code == 200, "Response code is %s" % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))["movies"][0]["movies_list_ids"][0] assert returned_identifier["id_name"], returned_identifier["id_value"] == identifier.items()[0] # Get specific movie from list rsp = api_client.get("/movie_list/1/movies/1/") assert rsp.status_code == 200, "Response code is %s" % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))["movies_list_ids"][0] assert returned_identifier["id_name"], returned_identifier["id_value"] == identifier.items()[0] identifiers = [{"trakt_movie_id": "12345"}] # Change specific movie from list rsp = api_client.json_put("/movie_list/1/movies/1/", data=json.dumps(identifiers)) assert rsp.status_code == 200, "Response code is %s" % rsp.status_code returned_identifier = json.loads(rsp.get_data(as_text=True))["movies_list_ids"][0] assert returned_identifier["id_name"], returned_identifier["id_value"] == identifiers[0].items() # Delete specific movie from list rsp = api_client.delete("/movie_list/1/movies/1/") assert rsp.status_code == 200, "Response code is %s" % rsp.status_code # Get non existent movie from list rsp = api_client.get("/movie_list/1/movies/1/") assert rsp.status_code == 404, "Response code is %s" % rsp.status_code # Delete non existent movie from list rsp = api_client.delete("/movie_list/1/movies/1/") assert rsp.status_code == 404, "Response code is %s" % rsp.status_code
def test_percent(self, api_client, schema_match): payload1 = {'percent': '79%'} rsp = api_client.json_post('/format_check/', data=json.dumps(payload1)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors payload2 = {'percent': 'bla'} rsp = api_client.json_post('/format_check/', data=json.dumps(payload2)) assert rsp.status_code == 422, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def send_push(self, task, auth_token, room_key, color, notify, title, message, url): body = '%s %s' % (title, message) data = {'color': color, 'message': body, 'notify': notify, 'message_format': "text"} # Check for test mode if task.options.test: log.info('Test mode. Hipchat notification would be:') log.info(' Auth Token: %s' % auth_token) log.info(' Room Key: %s' % room_key) log.info(' Color: %s' % color) log.info(' Notify: %s' % notify) log.info(' Title: %s' % title) log.info(' Message: %s' % message) log.info(' URL: %s' % url) log.info(' Raw Data: %s' % json.dumps(data)) # Test mode. Skip remainder. return # Make the request headers = { 'Content-Type': 'application/json' } response = task.requests.post(url, headers=headers, data=json.dumps(data), raise_status=False) # Check if it succeeded request_status = response.status_code # error codes and messages from Hipchat API if request_status == 200: log.debug('Hipchat notification sent') elif request_status == 500: log.warning('Hipchat notification failed, Hipchat API having issues') # TODO: Implement retrying. API requests 5 seconds between retries. elif request_status >= 400: if response.content: try: error = json.loads(response.content)['error'] except ValueError: error = 'Unknown Error (Invalid JSON returned)' log.error('Hipchat API error: %s' % error['message']) else: log.error('Unknown error when sending Hipchat notification')
def test_new_series_begin(self, execute_task, api_client): show = 'Test Show' new_show = { "series_name": show, "episode_identifier": "s01e01", "alternate_names": ['show1', 'show2'] } rsp = api_client.json_post(('/series/'), data=json.dumps(new_show)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code
def on_task_input(self, task, config): # Don't edit the config, or it won't pass validation on rerun url_params = config.copy() if 'movies' in config and 'series' in config: raise plugin.PluginError('Cannot use both series list and movies list in the same task.') if 'movies' in config: url_params['data_type'] = 'movies' url_params['list_type'] = config['movies'] map = self.movie_map elif 'series' in config: url_params['data_type'] = 'shows' url_params['list_type'] = config['series'] map = self.series_map elif 'custom' in config: url_params['data_type'] = 'custom' url_params['list_type'] = make_list_slug(config['custom']) # Map type is per item in custom lists else: raise plugin.PluginError('Must define movie or series lists to retrieve from trakt.') url = 'http://api.trakt.tv/user/' auth = None if url_params['data_type'] == 'custom': url += 'list.json/%(api_key)s/%(username)s/%(list_type)s' elif url_params['list_type'] == 'watchlist': url += 'watchlist/%(data_type)s.json/%(api_key)s/%(username)s' else: url += 'library/%(data_type)s/%(list_type)s.json/%(api_key)s/%(username)s' url = url % url_params if 'password' in config: auth = {'username': config['username'], 'password': hashlib.sha1(config['password']).hexdigest()} entries = [] log.verbose('Retrieving list %s %s...' % (url_params['data_type'], url_params['list_type'])) try: result = task.requests.post(url, data=json.dumps(auth)) except RequestException as e: raise plugin.PluginError('Could not retrieve list from trakt (%s)' % e.message) try: data = result.json() except ValueError: log.debug('Could not decode json from response: %s', data.text) raise plugin.PluginError('Error getting list from trakt.') def check_auth(): if task.requests.post( 'http://api.trakt.tv/account/test/' + config['api_key'], data=json.dumps(auth), raise_status=False ).status_code != 200: raise plugin.PluginError('Authentication to trakt failed.') if 'error' in data: check_auth() raise plugin.PluginError('Error getting trakt list: %s' % data['error']) if not data: check_auth() log.warning('No data returned from trakt.') return if url_params['data_type'] == 'custom': if not isinstance(data['items'], list): raise plugin.PluginError('Faulty custom items in response: %s' % data['items']) data = data['items'] for item in data: entry = Entry() if url_params['data_type'] == 'custom': if 'rating' in item: entry['trakt_in_collection'] = item['in_collection'] entry['trakt_in_watchlist'] = item['in_watchlist'] entry['trakt_rating'] = item['rating'] entry['trakt_rating_advanced'] = item['rating_advanced'] entry['trakt_watched'] = item['watched'] if item['type'] == 'movie': map = self.movie_map item = item['movie'] else: map = self.series_map item = item['show'] entry.update_using_map(map, item) if entry.isvalid(): if config.get('strip_dates'): # Remove year from end of name if present entry['title'] = re.sub('\s+\(\d{4}\)$', '', entry['title']) entries.append(entry) return entries
def on_task_output(self, task, config): """Finds accepted movies and series episodes and submits them to trakt as acquired.""" # Change password to an SHA1 digest of the password config['password'] = hashlib.sha1(config['password']).hexdigest() found = {} for entry in task.accepted: if config['type'] == 'series': # Check entry is a series episode if entry.get('series_name') and entry.get( 'series_id_type') == 'ep': series = found.setdefault(entry['series_name'], {}) if not series: # If this is the first episode found from this series, set the parameters series['title'] = entry.get('tvdb_series_name', entry['series_name']) if entry.get('imdb_id'): series['imdb_id'] = entry['imdb_id'] if entry.get('tvdb_id'): series['tvdb_id'] = entry['tvdb_id'] series['episodes'] = [] episode = { 'season': entry['series_season'], 'episode': entry['series_episode'] } series['episodes'].append(episode) log.debug( 'Marking %s S%02dE%02d for submission to trakt.tv library.' % (entry['series_name'], entry['series_season'], entry['series_episode'])) else: # Check entry is a movie if entry.get('imdb_id') or entry.get('tmdb_id'): movie = {} # We know imdb_id or tmdb_id is filled in, so don't cause any more lazy lookups if entry.get('movie_name', eval_lazy=False): movie['title'] = entry['movie_name'] if entry.get('movie_year', eval_lazy=False): movie['year'] = entry['movie_year'] if entry.get('tmdb_id', eval_lazy=False): movie['tmdb_id'] = entry['tmdb_id'] if entry.get('imdb_id', eval_lazy=False): movie['imdb_id'] = entry['imdb_id'] # We use an extra container dict so that the found dict is usable in the same way as found series found.setdefault('movies', {}).setdefault('movies', []).append(movie) log.debug( 'Marking %s for submission to trakt.tv library.' % entry['title']) if not found: log.debug('Nothing to submit to trakt.') return if task.options.test: log.info('Not submitting to trakt.tv because of test mode.') return # Submit our found items to trakt if config['type'] == 'series': post_url = 'http://api.trakt.tv/show/episode/library/' + config[ 'api_key'] else: post_url = 'http://api.trakt.tv/movie/library/' + config['api_key'] for item in found.itervalues(): # Add username and password to the dict to submit item.update({ 'username': config['username'], 'password': config['password'] }) try: result = task.requests.post(post_url, data=json.dumps(item), raise_status=False) except RequestException as e: log.error('Error submitting data to trakt.tv: %s' % e) continue if result.status_code == 404: # Remove some info from posted json and print the rest to aid debugging for key in ['username', 'password', 'episodes']: item.pop(key, None) log.warning('%s not found on trakt: %s' % (config['type'].capitalize(), item)) continue elif result.status_code == 401: log.error( 'Error authenticating with trakt. Check your username/password/api_key' ) log.debug(result.text) continue elif result.status_code != 200: log.error('Error submitting data to trakt.tv: %s' % result.text) continue
def test_movie_list_movie(self, api_client, schema_match): payload = {'name': 'name'} # Create list rsp = api_client.json_post('/movie_list/', data=json.dumps(payload)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code identifier = {'imdb_id': 'tt1234567'} movie_data = {'movie_name': 'title', 'movie_identifiers': [identifier]} # Add movie to list rsp = api_client.json_post('/movie_list/1/movies/', data=json.dumps(movie_data)) assert rsp.status_code == 201, 'Response code is %s' % rsp.status_code # Get specific movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors returned_identifier = data['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier[ 'id_value'] == identifier.items()[0] identifiers = [{'trakt_movie_id': '12345'}] # Change specific movie from list rsp = api_client.json_put('/movie_list/1/movies/1/', data=json.dumps(identifiers)) assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.movie_list_object, data) assert not errors returned_identifier = data['movies_list_ids'][0] assert returned_identifier['id_name'], returned_identifier[ 'id_value'] == identifiers[0].items() # PUT non-existent movie from list rsp = api_client.json_put('/movie_list/1/movies/10/', data=json.dumps(identifiers)) assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors non_valid_identifier = [{'bla': 'tt1234567'}] # Change movie using invalid identifier from list rsp = api_client.json_put('/movie_list/1/movies/1/', data=json.dumps(non_valid_identifier)) assert rsp.status_code == 400, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors # Delete specific movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 200, 'Response code is %s' % rsp.status_code data = json.loads(rsp.get_data(as_text=True)) errors = schema_match({'type': 'object'}, data) assert not errors # Get non existent movie from list rsp = api_client.get('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code # Delete non existent movie from list rsp = api_client.delete('/movie_list/1/movies/1/') assert rsp.status_code == 404, 'Response code is %s' % rsp.status_code
def add_entries(self, task, config): """Adds accepted entries""" apiurl = config.get('api', self.DEFAULT_API) api = PyloadApi(task.requests, apiurl) try: session = api.get_session(config) except OSError: raise plugin.PluginError('pyLoad not reachable', logger) except plugin.PluginError: raise except Exception as e: raise plugin.PluginError('Unknown error: %s' % str(e), logger) remote_version = None try: remote_version = api.get('getServerVersion') except RequestException as e: if e.response is not None and e.response.status_code == 404: remote_version = json.loads( api.get('get_server_version').content) else: raise e parse_urls_command = 'parseURLs' add_package_command = 'addPackage' set_package_data_command = 'setPackageData' is_pyload_ng = False version = self.get_version_from_packaging() if version and version.parse(remote_version) >= version.parse('0.5'): parse_urls_command = 'parse_urls' add_package_command = 'add_package' set_package_data_command = 'set_package_date' is_pyload_ng = True hoster = config.get('hoster', self.DEFAULT_HOSTER) for entry in task.accepted: # bunch of urls now going to check content = entry.get('description', '') + ' ' + quote(entry['url']) content = json.dumps(content) if is_pyload_ng: url = (entry['url'] if config.get( 'parse_url', self.DEFAULT_PARSE_URL) else '') else: url = (json.dumps(entry['url']) if config.get( 'parse_url', self.DEFAULT_PARSE_URL) else "''") logger.debug('Parsing url {}', url) data = {'html': content, 'url': url} if not is_pyload_ng: data['session'] = session result = api.post(parse_urls_command, data=data) parsed = result.json() urls = [] # check for preferred hoster for name in hoster: if name in parsed: urls.extend(parsed[name]) if not config.get('multiple_hoster', self.DEFAULT_MULTIPLE_HOSTER): break # no preferred hoster and not preferred hoster only - add all recognized plugins if not urls and not config.get('preferred_hoster_only', self.DEFAULT_PREFERRED_HOSTER_ONLY): for name, purls in parsed.items(): if name != 'BasePlugin': urls.extend(purls) if task.options.test: logger.info('Would add `{}` to pyload', urls) continue # no urls found if not urls: if config.get('handle_no_url_as_failure', self.DEFAULT_HANDLE_NO_URL_AS_FAILURE): entry.fail('No suited urls in entry %s' % entry['title']) else: logger.info('No suited urls in entry {}', entry['title']) continue logger.debug('Add {} urls to pyLoad', len(urls)) try: dest = 1 if config.get( 'queue', self.DEFAULT_QUEUE) else 0 # Destination.Queue = 1 # Use the title of the entry, if no naming schema for the package is defined. name = config.get('package', entry['title']) # If name has jinja template, render it try: name = entry.render(name) except RenderError as e: name = entry['title'] logger.error('Error rendering jinja event: {}', e) if is_pyload_ng: data = { 'name': name.encode('ascii', 'ignore').decode(), 'links': urls, 'dest': dest, } else: data = { 'name': json.dumps(name.encode('ascii', 'ignore').decode()), 'links': json.dumps(urls), 'dest': json.dumps(dest), 'session': session } pid = api.post(add_package_command, data=data).text logger.debug('added package pid: {}', pid) # Set Folder folder = config.get('folder', self.DEFAULT_FOLDER) folder = entry.get('path', folder) if folder: # If folder has jinja template, render it try: folder = entry.render(folder) except RenderError as e: folder = self.DEFAULT_FOLDER logger.error('Error rendering jinja event: {}', e) # set folder with api data = json.dumps({'folder': folder}) post_data = {'pid': pid, 'data': data} if not is_pyload_ng: post_data['session'] = session api.post(set_package_data_command, data=post_data) # Set Package Password package_password = config.get('package_password') if package_password: data = json.dumps({'password': package_password}) post_data = {'pid': pid, 'data': data} if not is_pyload_ng: post_data['session'] = session api.post(set_package_data_command, data=post_data) except Exception as e: entry.fail(str(e))
def test_json_encode_dt(self): date_str = '2016-03-11T17:12:17Z' dt = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%SZ') encoded_dt = json.dumps(dt, encode_datetime=True) assert encoded_dt == '"%s"' % date_str
def on_task_metainfo(self, task, config): if not task.entries: return url = 'http://api.trakt.tv/user/library/shows/collection.json/%s/%s' % \ (config['api_key'], config['username']) auth = None if 'password' in config: auth = { 'username': config['username'], 'password': hashlib.sha1(config['password']).hexdigest() } try: log.debug('Opening %s' % url) data = task.requests.get(url, data=json.dumps(auth)).json() except RequestException as e: raise plugin.PluginError('Unable to get data from trakt.tv: %s' % e) def check_auth(): if task.requests.post( 'http://api.trakt.tv/account/test/' + config['api_key'], data=json.dumps(auth), raise_status=False).status_code != 200: raise plugin.PluginError('Authentication to trakt failed.') if not data: check_auth() self.log.warning('No data returned from trakt.') return if 'error' in data: check_auth() raise plugin.PluginError('Error getting trakt list: %s' % data['error']) log.verbose('Received %d series records from trakt.tv' % len(data)) # the index will speed the work if we have a lot of entries to check index = {} for idx, val in enumerate(data): index[val['title']] = index[int( val['tvdb_id'])] = index[val['imdb_id']] = idx for entry in task.entries: if not (entry.get('series_name') and entry.get('series_season') and entry.get('series_episode')): continue entry['trakt_in_collection'] = False if 'tvdb_id' in entry and entry['tvdb_id'] in index: series = data[index[entry['tvdb_id']]] elif 'imdb_id' in entry and entry['imdb_id'] in index: series = data[index[entry['imdb_id']]] elif 'series_name' in entry and entry['series_name'] in index: series = data[index[entry['series_name']]] else: continue for s in series['seasons']: if s['season'] == entry['series_season']: entry['trakt_in_collection'] = entry[ 'series_episode'] in s['episodes'] break log.debug( 'The result for entry "%s" is: %s' % (entry['title'], 'Owned' if entry['trakt_in_collection'] else 'Not owned'))
def process_notifications(self, task, entries, config): for entry in entries: if task.options.test: log.info("Would send RapidPush notification about: %s", entry['title']) continue log.info("Send RapidPush notification about: %s", entry['title']) apikey = entry.get('apikey', config['apikey']) if isinstance(apikey, list): apikey = ','.join(apikey) title = config['title'] try: title = entry.render(title) except RenderError as e: log.error('Error setting RapidPush title: %s' % e) message = config['message'] try: message = entry.render(message) except RenderError as e: log.error('Error setting RapidPush message: %s' % e) # Check if we have to send a normal or a broadcast notification. if not config['channel']: priority = entry.get('priority', config['priority']) category = entry.get('category', config['category']) try: category = entry.render(category) except RenderError as e: log.error('Error setting RapidPush category: %s' % e) group = entry.get('group', config['group']) try: group = entry.render(group) except RenderError as e: log.error('Error setting RapidPush group: %s' % e) # Send the request data_string = json.dumps({ 'title': title, 'message': message, 'priority': priority, 'category': category, 'group': group}) data = {'apikey': apikey, 'command': 'notify', 'data': data_string} else: channel = config['channel'] try: channel = entry.render(channel) except RenderError as e: log.error('Error setting RapidPush channel: %s' % e) # Send the broadcast request data_string = json.dumps({ 'title': title, 'message': message, 'channel': channel}) data = {'apikey': apikey, 'command': 'broadcast', 'data': data_string} response = task.requests.post(url, data=data, raise_status=False) json_data = response.json() if 'code' in json_data: if json_data['code'] == 200: log.debug("RapidPush message sent") else: log.error(json_data['desc'] + " (" + str(json_data['code']) + ")") else: for item in json_data: if json_data[item]['code'] == 200: log.debug(item + ": RapidPush message sent") else: log.error(item + ": " + json_data[item]['desc'] + " (" + str(json_data[item]['code']) + ")")
def on_task_output(self, task, config): """Finds accepted movies and series episodes and submits them to trakt as acquired.""" # Change password to an SHA1 digest of the password config['password'] = hashlib.sha1(config['password']).hexdigest() # Don't edit the config, or it won't pass validation on rerun url_params = config.copy() url_params['data_type'] = 'list' # Do some translation from visible list name to prepare for use in url list_name = config['list'].lower() # These characters are just stripped in the url for char in '!@#$%^*()[]{}/=?+\\|-_': list_name = list_name.replace(char, '') # These characters get replaced list_name = list_name.replace('&', 'and') list_name = list_name.replace(' ', '-') url_params['list_type'] = list_name # Map type is per item in custom lists found = {} for entry in task.accepted: # if config['type'] == 'series': # # Check entry is a series episode # if entry.get('series_name') and entry.get('series_id_type') == 'ep': # series = found.setdefault(entry['series_name'], {}) # if not series: # # If this is the first episode found from this series, set the parameters # series['title'] = entry.get('tvdb_series_name', entry['series_name']) # if entry.get('imdb_id'): # series['imdb_id'] = entry['imdb_id'] # if entry.get('tvdb_id'): # series['tvdb_id'] = entry['tvdb_id'] # series['episodes'] = [] # episode = {'season': entry['series_season'], 'episode': entry['series_episode']} # series['episodes'].append(episode) # log.debug('Marking %s S%02dE%02d for submission to trakt.tv library.' % # (entry['series_name'], entry['series_season'], entry['series_episode'])) if config['type'] == 'movies': # Check entry is a movie if entry.get('imdb_id') or entry.get('tmdb_id'): movie = {} # We know imdb_id or tmdb_id is filled in, so don't cause any more lazy lookups if entry.get('movie_name', eval_lazy=False): movie['title'] = entry['movie_name'] if entry.get('movie_year', eval_lazy=False): movie['year'] = entry['movie_year'] if entry.get('tmdb_id', eval_lazy=False): movie['tmdb_id'] = entry['tmdb_id'] if entry.get('imdb_id', eval_lazy=False): movie['imdb_id'] = entry['imdb_id'] # We use an extra container dict so that the found dict is usable in the same way as found series if url_params['list'] == 'watchlist': found.setdefault('movies', {}).setdefault('movies', []).append(movie) else: movie['type'] = 'movie' found.setdefault('items', {}).setdefault('items', []).append(movie) log.debug( 'Marking %s for submission to trakt.tv library.' % entry['title']) # log.verbose('json dump (found) : %s' % json.dumps(found)) if not found: log.debug('Nothing to submit to trakt.') return if task.manager.options.test: log.info('Not submitting to trakt.tv because of test mode.') return # URL to remove collected entries from trakt list if url_params['list'] == 'watchlist': post_url = 'http://api.trakt.tv/movie/unwatchlist/' + config[ 'api_key'] else: post_url = 'http://api.trakt.tv/lists/items/delete/' + config[ 'api_key'] # Delete entry from list for item in found.itervalues(): # Add username, password and list (slug) to the dict to submit item.update({ 'username': config['username'], 'password': config['password'], 'slug': url_params['list'] }) try: result = task.requests.post(post_url, data=json.dumps(item), raise_status=False) except RequestException as e: log.error('Error submitting data to trakt.tv: %s' % e) continue if result.status_code == 404: # Remove some info from posted json and print the rest to aid debugging for key in ['username', 'password', 'episodes']: item.pop(key, None) log.warning('%s not found on trakt (remove_collected): %s' % (config['type'].capitalize(), item)) continue elif result.status_code == 401: log.error( 'Error authenticating with trakt (remove_collected). Check your username/password/api_key' ) log.debug(result.text) continue elif result.status_code != 200: log.error( 'Error submitting data to trakt.tv (remove_collected): %s' % result.text) continue
def write(self, s): self.put(json.dumps({'log': s}))
def on_task_output(self, task, config): """Submits accepted movies or episodes to trakt api.""" found = {'shows': [], 'movies': []} for entry in task.accepted: if 'series_name' in entry: show = { 'title': entry['series_name'], 'ids': get_entry_ids(entry) } if 'series_season' in entry: season = {'number': entry['series_season']} if 'series_episode' in entry: season['episodes'] = [{ 'number': entry['series_episode'] }] show['seasons'] = [season] found['shows'].append(show) elif any(field in entry for field in ['imdb_id', 'tmdb_id', 'movie_name']): movie = {'ids': get_entry_ids(entry)} if not movie['ids']: movie['title'] = entry.get('movie_name') or entry.get( 'imdb_name') movie['year'] = entry.get('movie_year') or entry.get( 'imdb_year') found['movies'].append(movie) if not (found['shows'] or found['movies']): self.log.debug('Nothing to submit to trakt.') return if config['list'] in ['collection', 'watchlist', 'watched']: endpoint = 'sync/%s' % ('history' if config['list'] == 'watched' else config['list']) else: endpoint = 'users/%s/lists/%s/items' % ( config['username'], make_list_slug(config['list'])) if self.remove: endpoint += '/remove' url = API_URL + endpoint if task.manager.options.test: self.log.info('Not submitting to trakt.tv because of test mode.') return session = get_session(config['username'], config['password']) self.log.debug('Submitting data to trakt.tv (%s): %s' % (url, found)) try: result = session.post(url, data=json.dumps(found), raise_status=False) except RequestException as e: self.log.error('Error submitting data to trakt.tv: %s' % e) return if 200 <= result.status_code < 300: self.log.info('Data successfully sent to trakt.tv') self.log.debug('trakt response: ' + result.text) # TODO: Improve messages about existing and unknown results elif result.status_code == 404: self.log.error('List does not appear to exist on trakt: %s' % config['list']) elif result.status_code == 401: self.log.error( 'Authentication error: check your trakt.tv username/password') self.log.debug('trakt response: ' + result.text) else: self.log.error('Unknown error submitting data to trakt.tv: %s' % result.text)
def test_pending_list_entry(self, api_client, schema_match): payload = {'name': 'test_list'} # Create list rsp = api_client.json_post('/pending_list/', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_base_object, data) assert not errors for field, value in payload.items(): assert data.get(field) == value entry_data = {'title': 'title', 'original_url': 'http://test.com'} # Add entry to list rsp = api_client.json_post('/pending_list/1/entries/', data=json.dumps(entry_data)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors for field, value in entry_data.items(): assert data.get(field) == value # Get entries from list rsp = api_client.get('/pending_list/1/entries/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors for field, value in entry_data.items(): assert data[0].get(field) == value # Get specific entry from list rsp = api_client.get('/pending_list/1/entries/1/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors for field, value in entry_data.items(): assert data.get(field) == value new_entry_data = {'operation': 'approve'} # Change specific entry from list rsp = api_client.json_put('/pending_list/1/entries/1/', data=json.dumps(new_entry_data)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors assert data['approved'] # Try to change non-existent entry from list rsp = api_client.json_put('/pending_list/1/entries/10/', data=json.dumps(new_entry_data)) assert rsp.status_code == 404 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors # Delete specific entry from list rsp = api_client.delete('/pending_list/1/entries/1/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors # Get non existent entry from list rsp = api_client.get('/pending_list/1/entries/1/') assert rsp.status_code == 404 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors # Delete non existent entry from list rsp = api_client.delete('/pending_list/1/entries/1/') assert rsp.status_code == 404 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors
def send_push(self, task, api_key, title, body, url=None, destination=None, destination_type=None): if url: push_type = 'link' else: push_type = 'note' data = {'type': push_type, 'title': title, 'body': body} if url: data['url'] = url if destination: data[destination_type] = destination # Check for test mode if task.options.test: log.info('Test mode. Pushbullet notification would be:') log.info(' API Key: %s' % api_key) log.info(' Type: %s' % push_type) log.info(' Title: %s' % title) log.info(' Body: %s' % body) if destination: log.info(' Destination: %s (%s)' % (destination, destination_type)) if url: log.info(' URL: %s' % url) log.info(' Raw Data: %s' % json.dumps(data)) # Test mode. Skip remainder. return # Make the request headers = { 'Authorization': b'Basic ' + base64.b64encode(api_key.encode('ascii')), 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Flexget' } response = task.requests.post(pushbullet_url, headers=headers, data=json.dumps(data), raise_status=False) # Check if it succeeded request_status = response.status_code # error codes and messages from Pushbullet API if request_status == 200: log.debug('Pushbullet notification sent') elif request_status == 500: log.warning( 'Pushbullet notification failed, Pushbullet API having issues') # TODO: Implement retrying. API requests 5 seconds between retries. elif request_status >= 400: error = 'Unknown error' if response.content: try: error = response.json()['error']['message'] except ValueError as e: error = 'Unknown Error (Invalid JSON returned): %s' % e log.error('Pushbullet API error: %s' % error) else: log.error('Unknown error when sending Pushbullet notification')
def test_json_encode_dt_dict(self): date_str = '2016-03-11T17:12:17Z' dt = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%SZ') date_obj = {'date': dt} encoded_dt = json.dumps(date_obj, encode_datetime=True) assert encoded_dt == '{"date": "%s"}' % date_str
def on_task_input(self, task, config): # Don't edit the config, or it won't pass validation on rerun url_params = config.copy() if 'movies' in config and 'series' in config: raise PluginError( 'Cannot use both series list and movies list in the same task.' ) if 'movies' in config: url_params['data_type'] = 'movies' url_params['list_type'] = config['movies'] map = self.movie_map elif 'series' in config: url_params['data_type'] = 'shows' url_params['list_type'] = config['series'] map = self.series_map elif 'custom' in config: url_params['data_type'] = 'custom' # Do some translation from visible list name to prepare for use in url list_name = config['custom'].lower() # These characters are just stripped in the url for char in '!@#$%^*()[]{}/=?+\\|-_': list_name = list_name.replace(char, '') # These characters get replaced list_name = list_name.replace('&', 'and') list_name = list_name.replace(' ', '-') url_params['list_type'] = list_name # Map type is per item in custom lists else: raise PluginError( 'Must define movie or series lists to retrieve from trakt.') url = 'http://api.trakt.tv/user/' auth = None if url_params['data_type'] == 'custom': url += 'list.json/%(api_key)s/%(username)s/%(list_type)s' elif url_params['list_type'] == 'watchlist': url += 'watchlist/%(data_type)s.json/%(api_key)s/%(username)s' else: url += 'library/%(data_type)s/%(list_type)s.json/%(api_key)s/%(username)s' url = url % url_params if 'password' in config: auth = { 'username': config['username'], 'password': hashlib.sha1(config['password']).hexdigest() } entries = [] log.verbose('Retrieving list %s %s...' % (url_params['data_type'], url_params['list_type'])) result = task.requests.get(url, data=json.dumps(auth)) try: data = task.requests.post(url, data=json.dumps(auth)).json() except RequestException as e: raise PluginError('Could not retrieve list from trakt (%s)' % e.message) def check_auth(): if task.requests.post( 'http://api.trakt.tv/account/test/' + config['api_key'], data=json.dumps(auth), raise_status=False).status_code != 200: raise PluginError('Authentication to trakt failed.') if 'error' in data: check_auth() raise PluginError('Error getting trakt list: %s' % data['error']) if not data: check_auth() log.warning('No data returned from trakt.') return if url_params['data_type'] == 'custom': if not isinstance(data['items'], list): raise PluginError('Faulty custom items in response: %s' % data['items']) data = data['items'] for item in data: if url_params['data_type'] == 'custom': if item['type'] == 'movie': map = self.movie_map item = item['movie'] else: map = self.series_map item = item['show'] entry = Entry() entry.update_using_map(map, item) if entry.isvalid(): if config.get('strip_dates'): # Remove year from end of name if present entry['title'] = re.sub('\s+\(\d{4}\)$', '', entry['title']) entries.append(entry) return entries
def post_json_to_trakt(self, url, data): """Dumps data as json and POSTs it to the specified url.""" req = urllib2.Request(url, json.dumps(data), {'content-type': 'application/json'}) return urlopener(req, log)
def submit(self, entries, remove=False): """Submits movies or episodes to trakt api.""" found = {} for entry in entries: if self.config['type'] in ['auto', 'shows', 'seasons', 'episodes'] and entry.get('series_name') is not None: show_name, show_year = split_title_year(entry['series_name']) show = {'title': show_name, 'ids': get_entry_ids(entry)} if show_year: show['year'] = show_year if self.config['type'] in ['auto', 'seasons', 'episodes'] and entry.get('series_season') is not None: season = {'number': entry['series_season']} if self.config['type'] in ['auto', 'episodes'] and entry.get('series_episode') is not None: season['episodes'] = [{'number': entry['series_episode']}] show['seasons'] = [season] if self.config['type'] in ['seasons', 'episodes'] and 'seasons' not in show: log.debug('Not submitting `%s`, no season found.' % entry['title']) continue if self.config['type'] == 'episodes' and 'episodes' not in show: log.debug('Not submitting `%s`, no episode number found.' % entry['title']) continue found.setdefault('shows', []).append(show) elif self.config['type'] in ['auto', 'movies']: movie = {'ids': get_entry_ids(entry)} if not movie['ids']: if entry.get('movie_name') is not None: movie['title'] = entry.get('movie_name') or entry.get('imdb_name') movie['year'] = entry.get('movie_year') or entry.get('imdb_year') else: log.debug('Not submitting `%s`, no movie name or id found.' % entry['title']) continue found.setdefault('movies', []).append(movie) if not (found.get('shows') or found.get('movies')): log.debug('Nothing to submit to trakt.') return if self.config['list'] in ['collection', 'watchlist', 'watched']: args = ('sync', 'history' if self.config['list'] == 'watched' else self.config['list']) else: args = ('users', self.config['username'], 'lists', make_list_slug(self.config['list']), 'items') if remove: args += ('remove',) url = get_api_url(args) log.debug('Submitting data to trakt.tv (%s): %s' % (url, found)) try: result = self.session.post(url, data=json.dumps(found), raise_status=False) except RequestException as e: log.error('Error submitting data to trakt.tv: %s' % e) return if 200 <= result.status_code < 300: action = 'deleted' if remove else 'added' res = result.json() # Default to 0 for all categories, even if trakt response didn't include them for cat in ('movies', 'shows', 'episodes', 'seasons'): res[action].setdefault(cat, 0) log.info('Successfully {0} to/from list {1}: {movies} movie(s), {shows} show(s), {episodes} episode(s), ' '{seasons} season(s).'.format(action, self.config['list'], **res[action])) for k, r in res['not_found'].items(): if r: log.debug('not found %s: %s' % (k, r)) # TODO: Improve messages about existing and unknown results # Mark the results expired if we added or removed anything if sum(res[action].values()) > 0: self.invalidate_cache() elif result.status_code == 404: log.error('List does not appear to exist on trakt: %s' % self.config['list']) elif result.status_code == 401: log.error('Authentication error: have you authorized Flexget on Trakt.tv?') log.debug('trakt response: ' + result.text) else: log.error('Unknown error submitting data to trakt.tv: %s' % result.text)
def check_auth(): if task.requests.post( 'http://api.trakt.tv/account/test/' + config['api_key'], data=json.dumps(auth), raise_status=False ).status_code != 200: raise plugin.PluginError('Authentication to trakt failed.')
def add_entries(self, task, config): """Adds accepted entries""" try: session = self.get_session(config) except IOError: raise plugin.PluginError('pyLoad not reachable', log) except plugin.PluginError: raise except Exception as e: raise plugin.PluginError('Unknown error: %s' % str(e), log) api = config.get('api', self.DEFAULT_API) hoster = config.get('hoster', self.DEFAULT_HOSTER) for entry in task.accepted: # bunch of urls now going to check content = entry.get('description', '') + ' ' + quote(entry['url']) content = json.dumps(content.encode("utf8")) url = json.dumps(entry['url']) if config.get( 'parse_url', self.DEFAULT_PARSE_URL) else "''" log.debug("Parsing url %s" % url) result = query_api(api, "parseURLs", { "html": content, "url": url, "session": session }) # parsed { plugins: [urls] } parsed = result.json() urls = [] # check for preferred hoster for name in hoster: if name in parsed: urls.extend(parsed[name]) if not config.get('multiple_hoster', self.DEFAULT_MULTIPLE_HOSTER): break # no preferred hoster and not preferred hoster only - add all recognized plugins if not urls and not config.get('preferred_hoster_only', self.DEFAULT_PREFERRED_HOSTER_ONLY): for name, purls in parsed.iteritems(): if name != "BasePlugin": urls.extend(purls) if task.options.test: log.info('Would add `%s` to pyload' % urls) continue # no urls found if not urls: if config.get('handle_no_url_as_failure', self.DEFAULT_HANDLE_NO_URL_AS_FAILURE): entry.fail("No suited urls in entry %s" % entry['title']) else: log.info("No suited urls in entry %s" % entry['title']) continue log.debug("Add %d urls to pyLoad" % len(urls)) try: dest = 1 if config.get( 'queue', self.DEFAULT_QUEUE) else 0 # Destination.Queue = 1 # Use the title of the entry, if no naming schema for the package is defined. name = config.get('package', entry['title']) # If name has jinja template, render it try: name = entry.render(name) except RenderError as e: name = entry['title'] log.error('Error rendering jinja event: %s' % e) post = { 'name': "'%s'" % name.encode("ascii", "ignore"), 'links': str(urls), 'dest': dest, 'session': session } pid = query_api(api, "addPackage", post).text log.debug('added package pid: %s' % pid) # Set Folder folder = config.get('folder', self.DEFAULT_FOLDER) folder = entry.get('path', folder) if folder: # If folder has jinja template, render it try: folder = entry.render(folder) except RenderError as e: folder = self.DEFAULT_FOLDER log.error('Error rendering jinja event: %s' % e) # set folder with api data = json.dumps({'folder': folder}) query_api(api, "setPackageData", { 'pid': pid, 'data': data, 'session': session }) except Exception as e: entry.fail(str(e))
def process(self): imdb_lookup = plugin.get_plugin_by_name('imdb_lookup').instance self.changes.sort() udata = load_uoccin_data(self.folder) for line in self.changes: tmp = line.split('|') typ = tmp[1] tid = tmp[2] fld = tmp[3] val = tmp[4] self.log.verbose( 'processing: type=%s, target=%s, field=%s, value=%s' % (typ, tid, fld, val)) if typ == 'movie': # default mov = udata['movies'].setdefault( tid, { 'name': 'N/A', 'watchlist': False, 'collected': False, 'watched': False }) # movie title is unknown at this time fake = Entry() fake['url'] = 'http://www.imdb.com/title/' + tid fake['imdb_id'] = tid try: imdb_lookup.lookup(fake) mov['name'] = fake.get('imdb_name') except plugin.PluginError: self.log.warning( 'Unable to lookup movie %s from imdb, using raw name.' % tid) # setting if fld == 'watchlist': mov['watchlist'] = val == 'true' elif fld == 'collected': mov['collected'] = val == 'true' elif fld == 'watched': mov['watched'] = val == 'true' elif fld == 'tags': mov['tags'] = re.split(',\s*', val) elif fld == 'subtitles': mov['subtitles'] = re.split(',\s*', val) elif fld == 'rating': mov['rating'] = int(val) # cleaning if not (mov['watchlist'] or mov['collected'] or mov['watched']): self.log.verbose('deleting unused section: movies\%s' % tid) udata['movies'].pop(tid) elif typ == 'series': tmp = tid.split('.') sid = tmp[0] sno = tmp[1] if len(tmp) > 2 else None eno = tmp[2] if len(tmp) > 2 else None # default ser = udata['series'].setdefault( sid, { 'name': 'N/A', 'watchlist': False, 'collected': {}, 'watched': {} }) # series name is unknown at this time try: series = lookup_series(tvdb_id=sid) ser['name'] = series.name except LookupError: self.log.warning( 'Unable to lookup series %s from tvdb, using raw name.' % sid) # setting if fld == 'watchlist': ser['watchlist'] = val == 'true' elif fld == 'tags': ser['tags'] = re.split(',\s*', val) elif fld == 'rating': ser['rating'] = int(val) elif sno is None or eno is None: self.log.warning( 'invalid line "%s": season and episode numbers are required' % line) elif fld == 'collected': season = ser['collected'].setdefault(sno, {}) if val == 'true': season.setdefault(eno, []) else: if eno in season: season.pop(eno) if not season: self.log.verbose( 'deleting unused section: series\%s\collected\%s' % (sid, sno)) ser['collected'].pop(sno) elif fld == 'subtitles': ser['collected'].setdefault(sno, {})[eno] = re.split( ',\s*', val) elif fld == 'watched': season = ser['watched'].setdefault(sno, []) if val == 'true': season = ser['watched'][sno] = list( set(season) | set([int(eno)])) elif int(eno) in season: season.remove(int(eno)) season.sort() if not season: self.log.debug( 'deleting unused section: series\%s\watched\%s' % (sid, sno)) ser['watched'].pop(sno) # cleaning if not (ser['watchlist'] or ser['collected'] or ser['watched']): self.log.debug('deleting unused section: series\%s' % sid) udata['series'].pop(sid) else: self.log.warning('invalid element type "%s"' % typ) # save the updated uoccin.json ufile = os.path.join(self.folder, 'uoccin.json') try: text = json.dumps(udata, sort_keys=True, indent=4, separators=(',', ': ')) with open(ufile, 'w') as f: f.write(text) except Exception as err: self.log.debug('error writing %s: %s' % (ufile, err)) raise plugin.PluginError('error writing %s: %s' % (ufile, err))
class PluginPyLoad(object): """ Parse task content or url for hoster links and adds them to pyLoad. Example:: pyload: api: http://localhost:8000/api queue: yes username: my_username password: my_password folder: desired_folder hoster: - YoutubeCom parse_url: no multiple_hoster: yes enabled: yes Default values for the config elements:: pyload: api: http://localhost:8000/api queue: no hoster: ALL parse_url: no multiple_hoster: yes enabled: yes """ __author__ = 'http://pyload.org' __version__ = '0.3' DEFAULT_API = 'http://localhost:8000/api' DEFAULT_QUEUE = False DEFAULT_FOLDER = '' DEFAULT_HOSTER = [] DEFAULT_PARSE_URL = False DEFAULT_MULTIPLE_HOSTER = True def __init__(self): self.session = None def validator(self): """Return config validator""" root = validator.factory() root.accept('boolean') advanced = root.accept('dict') advanced.accept('text', key='api') advanced.accept('text', key='username') advanced.accept('text', key='password') advanced.accept('text', key='folder') advanced.accept('boolean', key='queue') advanced.accept('boolean', key='parse_url') advanced.accept('boolean', key='multiple_hoster') advanced.accept('list', key='hoster').accept('text') return root def on_process_start(self, task, config): self.session = None def on_task_output(self, task, config): if not config.get('enabled', True): return if not task.accepted: return self.add_entries(task, config) def add_entries(self, task, config): """Adds accepted entries""" try: self.check_login(task, config) except URLError: raise PluginError('pyLoad not reachable', log) except PluginError: raise except Exception, e: raise PluginError('Unknown error: %s' % str(e), log) api = config.get('api', self.DEFAULT_API) hoster = config.get('hoster', self.DEFAULT_HOSTER) folder = config.get('folder', self.DEFAULT_FOLDER) for entry in task.accepted: # bunch of urls now going to check content = entry.get('description', '') + ' ' + quote(entry['url']) content = json.dumps(content.encode("utf8")) url = json.dumps(entry['url']) if config.get('parse_url', self.DEFAULT_PARSE_URL) else "''" log.debug("Parsing url %s" % url) result = query_api(api, "parseURLs", {"html": content, "url": url, "session": self.session}) # parsed { plugins: [urls] } parsed = json.loads(result.read()) urls = [] # check for preferred hoster for name in hoster: if name in parsed: urls.extend(parsed[name]) if not config.get('multiple_hoster', self.DEFAULT_MULTIPLE_HOSTER): break # no preferred hoster, add all recognized plugins if not urls: for name, purls in parsed.iteritems(): if name != "BasePlugin": urls.extend(purls) if task.manager.options.test: log.info('Would add `%s` to pyload' % urls) continue # no urls found if not urls: log.info("No suited urls in entry %s" % entry['title']) continue log.debug("Add %d urls to pyLoad" % len(urls)) try: dest = 1 if config.get('queue', self.DEFAULT_QUEUE) else 0 # Destination.Queue = 1 post = {'name': "'%s'" % entry['title'], 'links': str(urls), 'dest': dest, 'session': self.session} pid = query_api(api, "addPackage", post).read() log.debug('added package pid: %s' % pid) if folder: # set folder with api data = {'folder': folder} query_api(api, "setPackageData", {'pid': pid, 'data': data, 'session': self.session}) except Exception, e: task.fail(entry, str(e))
def job_id(conf): """Create a unique id for a schedule item in config.""" return hashlib.sha1(json.dumps(conf, sort_keys=True)).hexdigest()
def search(self, task, entry, config): task.requests.add_domain_limiter(self.request_limiter) config = self.prepare_config(config) api_key = config['api_key'] searches = entry.get('search_strings', [entry['title']]) if 'series_name' in entry: if entry.get('season_pack_lookup', False): search = {'category': 'Season'} else: search = {'category': 'Episode'} if 'tvdb_id' in entry: search['tvdb'] = entry['tvdb_id'] elif 'tvrage_id' in entry: search['tvrage'] = entry['tvrage_id'] else: search['series'] = entry['series_name'] if entry.get('season_pack_lookup', False) and 'series_season' in entry: search['name'] = 'Season %s' % entry['series_season'] elif 'series_id' in entry: # BTN wants an ep style identifier even for sequence shows if entry.get('series_id_type') == 'sequence': search['name'] = 'S01E%02d' % entry['series_id'] else: search['name'] = entry[ 'series_id'] + '%' # added wildcard search for better results. searches = [search] # If searching by series name ending in a parenthetical, try again without it if there are no results. if search.get('series') and search['series'].endswith(')'): match = re.match('(.+)\([^\(\)]+\)$', search['series']) if match: searches.append(dict(search, series=match.group(1).strip())) results = set() for search in searches: data = json.dumps({ 'method': 'getTorrents', 'params': [api_key, search], 'id': 1 }) try: r = task.requests.post( 'https://api.broadcasthe.net/', data=data, headers={'Content-type': 'application/json'}) except requests.RequestException as e: log.error('Error searching btn: %s' % e) continue try: content = r.json() except ValueError as e: raise plugin.PluginError( 'Error searching btn. Maybe it\'s down?. %s' % str(e)) if not content or not content['result']: log.debug('No results from btn') if content and content.get('error'): if content['error'].get('code') == -32002: log.error( 'btn api call limit exceeded, throttling connection rate' ) self.request_limiter.tokens = -1 else: log.error( 'Error searching btn: %s' % content['error'].get('message', content['error'])) continue if 'torrents' in content['result']: for item in content['result']['torrents'].values(): entry = Entry() entry['title'] = item['ReleaseName'] if config['append_quality']: entry['title'] += ' '.join([ '', item['Resolution'], item['Source'], item['Codec'] ]) entry['url'] = item['DownloadURL'] entry['torrent_seeds'] = int(item['Seeders']) entry['torrent_leeches'] = int(item['Leechers']) entry['torrent_info_hash'] = item['InfoHash'] entry['search_sort'] = torrent_availability( entry['torrent_seeds'], entry['torrent_leeches']) if item['TvdbID'] and int(item['TvdbID']): entry['tvdb_id'] = int(item['TvdbID']) if item['TvrageID'] and int(item['TvrageID']): entry['tvrage_id'] = int(item['TvrageID']) results.add(entry) # Don't continue searching if this search yielded results break return results
def test_pending_list_entries_batch_operation(self, api_client, schema_match): payload = {'name': 'test_list'} # Create list api_client.json_post('/pending_list/', data=json.dumps(payload)) # Add 3 entries to list for i in range(3): payload = { 'title': f'title {i}', 'original_url': f'http://{i}test.com' } rsp = api_client.json_post('/pending_list/1/entries/', data=json.dumps(payload)) assert rsp.status_code == 201 payload = {'operation': 'approve', 'ids': [1, 2, 3]} # Approve several entries rsp = api_client.json_put('/pending_list/1/entries/batch/', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors assert len(data) == 3 assert all((item['approved'] for item in data)) # get entries is correct rsp = api_client.get('/pending_list/1/entries/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors assert len(data) == 3 for item in data: assert item.get('approved') payload['operation'] = 'reject' # reject several entries rsp = api_client.json_put('pending_list/1/entries/batch', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors assert len(data) == 3 for item in data: assert not item.get('approved') rsp = api_client.get('/pending_list/1/entries/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors assert len(data) == 3 for item in data: assert not item.get('approved')
def add_entries(self, task, config): """Adds accepted entries""" apiurl = config.get('api', self.DEFAULT_API) api = PyloadApi(task.requests, apiurl) try: session = api.get_session(config) except IOError: raise plugin.PluginError('pyLoad not reachable', log) except plugin.PluginError: raise except Exception as e: raise plugin.PluginError('Unknown error: %s' % str(e), log) hoster = config.get('hoster', self.DEFAULT_HOSTER) for entry in task.accepted: # bunch of urls now going to check content = entry.get('description', '') + ' ' + quote(entry['url']) content = json.dumps(content) url = ( json.dumps(entry['url']) if config.get('parse_url', self.DEFAULT_PARSE_URL) else "''" ) log.debug('Parsing url %s', url) data = {'html': content, 'url': url, 'session': session} result = api.post('parseURLs', data=data) parsed = result.json() urls = [] # check for preferred hoster for name in hoster: if name in parsed: urls.extend(parsed[name]) if not config.get('multiple_hoster', self.DEFAULT_MULTIPLE_HOSTER): break # no preferred hoster and not preferred hoster only - add all recognized plugins if not urls and not config.get( 'preferred_hoster_only', self.DEFAULT_PREFERRED_HOSTER_ONLY ): for name, purls in parsed.items(): if name != 'BasePlugin': urls.extend(purls) if task.options.test: log.info('Would add `%s` to pyload', urls) continue # no urls found if not urls: if config.get('handle_no_url_as_failure', self.DEFAULT_HANDLE_NO_URL_AS_FAILURE): entry.fail('No suited urls in entry %s' % entry['title']) else: log.info('No suited urls in entry %s', entry['title']) continue log.debug('Add %d urls to pyLoad', len(urls)) try: dest = 1 if config.get('queue', self.DEFAULT_QUEUE) else 0 # Destination.Queue = 1 # Use the title of the entry, if no naming schema for the package is defined. name = config.get('package', entry['title']) # If name has jinja template, render it try: name = entry.render(name) except RenderError as e: name = entry['title'] log.error('Error rendering jinja event: %s', e) data = { 'name': json.dumps(name.encode('ascii', 'ignore').decode()), 'links': json.dumps(urls), 'dest': json.dumps(dest), 'session': session, } pid = api.post('addPackage', data=data).text log.debug('added package pid: %s', pid) # Set Folder folder = config.get('folder', self.DEFAULT_FOLDER) folder = entry.get('path', folder) if folder: # If folder has jinja template, render it try: folder = entry.render(folder) except RenderError as e: folder = self.DEFAULT_FOLDER log.error('Error rendering jinja event: %s', e) # set folder with api data = json.dumps({'folder': folder}) api.post("setPackageData", data={'pid': pid, 'data': data, 'session': session}) # Set Package Password package_password = config.get('package_password') if package_password: data = json.dumps({'password': package_password}) api.post('setPackageData', data={'pid': pid, 'data': data, 'session': session}) except Exception as e: entry.fail(str(e))
def submit(self, entries, remove=False): """Submits movies or episodes to trakt api.""" found = {} for entry in entries: if self.config['type'] in ['auto', 'shows', 'seasons', 'episodes'] and entry.get( 'series_name' ): show_name, show_year = split_title_year(entry['series_name']) show = {'title': show_name, 'ids': db.get_entry_ids(entry)} if show_year: show['year'] = show_year if ( self.config['type'] in ['auto', 'seasons', 'episodes'] and entry.get('series_season') is not None ): season = {'number': entry['series_season']} if ( self.config['type'] in ['auto', 'episodes'] and entry.get('series_episode') is not None ): season['episodes'] = [{'number': entry['series_episode']}] show['seasons'] = [season] if self.config['type'] in ['seasons', 'episodes'] and 'seasons' not in show: logger.debug('Not submitting `{}`, no season found.', entry['title']) continue if self.config['type'] == 'episodes' and 'episodes' not in show['seasons'][0]: logger.debug('Not submitting `{}`, no episode number found.', entry['title']) continue found.setdefault('shows', []).append(show) elif self.config['type'] in ['auto', 'movies']: movie = {'ids': db.get_entry_ids(entry)} if not movie['ids']: if entry.get('movie_name') is not None: movie['title'] = entry.get('movie_name') or entry.get('imdb_name') movie['year'] = entry.get('movie_year') or entry.get('imdb_year') else: logger.debug( 'Not submitting `{}`, no movie name or id found.', entry['title'] ) continue found.setdefault('movies', []).append(movie) if not (found.get('shows') or found.get('movies')): logger.debug('Nothing to submit to trakt.') return url = db.get_api_url(self.get_list_endpoint(remove, submit=True)) logger.debug('Submitting data to trakt.tv ({}): {}', url, found) try: result = self.session.post(url, data=json.dumps(found), raise_status=False) except RequestException as e: logger.error('Error submitting data to trakt.tv: {}', e) return if 200 <= result.status_code < 300: action = 'deleted' if remove else 'added' res = result.json() # Default to 0 for all categories, even if trakt response didn't include them for cat in ('movies', 'shows', 'episodes', 'seasons'): res[action].setdefault(cat, 0) logger.info( 'Successfully {0} to/from list {1}: {movies} movie(s), {shows} show(s), {episodes} episode(s), ' '{seasons} season(s).', action, self.config['list'], **res[action], ) for media_type, request in res['not_found'].items(): if request: logger.debug('not found {}: {}', media_type, request) # TODO: Improve messages about existing and unknown results # Mark the results expired if we added or removed anything if sum(res[action].values()): self.invalidate_cache() elif result.status_code == 404: logger.error('List does not appear to exist on trakt: {}', self.config['list']) elif result.status_code == 401: logger.error('Authentication error: have you authorized Flexget on Trakt.tv?') logger.debug('trakt response: {}', result.text) else: logger.error('Unknown error submitting data to trakt.tv: {}', result.text)
def test_pending_list_entries(self, api_client, schema_match): # Get non existent list rsp = api_client.get('/pending_list/1/entries/') assert rsp.status_code == 404 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(base_message, data) assert not errors payload = {'name': 'test_list'} # Create list rsp = api_client.json_post('/pending_list/', data=json.dumps(payload)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_base_object, data) assert not errors for field, value in payload.items(): assert data.get(field) == value entry_data = {'title': 'title', 'original_url': 'http://test.com'} # Add entry to list rsp = api_client.json_post('/pending_list/1/entries/', data=json.dumps(entry_data)) assert rsp.status_code == 201 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors for field, value in entry_data.items(): assert data.get(field) == value # Get entries from list rsp = api_client.get('/pending_list/1/entries/') assert rsp.status_code == 200 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_lists_entries_return_object, data) assert not errors for field, value in entry_data.items(): assert data[0].get(field) == value # Try to re-add entry to list rsp = api_client.json_post('/pending_list/1/entries/', data=json.dumps(entry_data)) assert rsp.status_code == 409 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors # Try to post to non existing list rsp = api_client.json_post('/pending_list/10/entries/', data=json.dumps(entry_data)) assert rsp.status_code == 404 data = json.loads(rsp.get_data(as_text=True)) errors = schema_match(OC.pending_list_entry_base_object, data) assert not errors
def setter(self, entry): setattr(self, name, json.dumps(entry, encode_datetime=True))
def on_task_output(self, task, config): """Submits accepted movies or episodes to trakt api.""" if config.get('account') and not config.get('username'): config['username'] = '******' found = {'shows': [], 'movies': []} for entry in task.accepted: if 'series_name' in entry: show = { 'title': entry['series_name'], 'ids': get_entry_ids(entry) } if 'series_season' in entry: season = {'number': entry['series_season']} if 'series_episode' in entry: season['episodes'] = [{ 'number': entry['series_episode'] }] show['seasons'] = [season] found['shows'].append(show) elif any(field in entry for field in ['imdb_id', 'tmdb_id', 'movie_name']): movie = {'ids': get_entry_ids(entry)} if not movie['ids']: movie['title'] = entry.get('movie_name') or entry.get( 'imdb_name') movie['year'] = entry.get('movie_year') or entry.get( 'imdb_year') found['movies'].append(movie) if not (found['shows'] or found['movies']): self.log.debug('Nothing to submit to trakt.') return if config['list'] in ['collection', 'watchlist', 'watched']: args = ('sync', 'history' if config['list'] == 'watched' else config['list']) else: args = ('users', config['username'], 'lists', make_list_slug(config['list']), 'items') if self.remove: args += ('remove', ) url = get_api_url(args) if task.manager.options.test: self.log.info('Not submitting to trakt.tv because of test mode.') return session = get_session(account=config.get('account')) self.log.debug('Submitting data to trakt.tv (%s): %s' % (url, found)) try: result = session.post(url, data=json.dumps(found), raise_status=False) except RequestException as e: self.log.error('Error submitting data to trakt.tv: %s' % e) return if 200 <= result.status_code < 300: action = 'added' if self.remove: action = 'deleted' res = result.json() movies = res[action].get('movies', 0) eps = res[action].get('episodes', 0) self.log.info( 'Successfully %s to/from list %s: %s movie(s), %s episode(s).' % (action, config['list'], movies, eps)) for k, r in res['not_found'].iteritems(): if r: self.log.debug('not found %s: %s' % (k, r)) # TODO: Improve messages about existing and unknown results elif result.status_code == 404: self.log.error('List does not appear to exist on trakt: %s' % config['list']) elif result.status_code == 401: self.log.error( 'Authentication error: have you authorized Flexget on Trakt.tv?' ) self.log.debug('trakt response: ' + result.text) else: self.log.error('Unknown error submitting data to trakt.tv: %s' % result.text)