class UrlRewriteDescargas2020(object): """Descargas2020 urlrewriter and search.""" schema = {'type': 'boolean', 'default': False} def __init__(self): self._session = None @property def session(self): # TODO: This is not used for all requests even .. if self._session is None: self._session = Session() self._session.headers.update({ 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' }) self._session.add_domain_limiter( TimedLimiter('descargas2020.com', '2 seconds')) return self._session # urlrewriter API def url_rewritable(self, task, entry): url = entry['url'] return not url.endswith('.torrent') and REWRITABLE_REGEX.match(url) # urlrewriter API def url_rewrite(self, task, entry): entry['url'] = self.parse_download_page(entry['url'], task) @plugin.internet(log) def parse_download_page(self, url, task): log.verbose('Descargas2020 URL: %s', url) try: page = self.session.get(url) except requests.RequestException as e: raise UrlRewritingError(e) try: soup = get_soup(page.text) except Exception as e: raise UrlRewritingError(e) torrent_id = None url_format = DESCARGAS2020_TORRENT_FORMAT torrent_id_prog = re.compile( r"(?:parametros\s*=\s*\n?)\s*{\s*\n(?:\s*'\w+'\s*:.*\n)+\s*'(?:torrentID|id)'\s*:\s*'(\d+)'" ) torrent_ids = soup.findAll(text=torrent_id_prog) if torrent_ids: match = torrent_id_prog.search(torrent_ids[0]) if match: torrent_id = match.group(1) if not torrent_id: log.debug('torrent ID not found, searching openTorrent script') match = re.search( r'function openTorrent.*\n.*\{.*(\n.*)+window\.location\.href =\s*\".*\/(\d+.*)\";', page.text, re.MULTILINE, ) if match: torrent_id = match.group(2).rstrip('/') if not torrent_id: raise UrlRewritingError('Unable to locate torrent ID from url %s' % url) return url_format.format(torrent_id) def search(self, task, entry, config=None): if not config: log.debug('Descargas2020 disabled') return set() log.debug('Search Descargas2020') url_search = 'http://descargas2020.com/buscar' results = set() for search_string in entry.get('search_strings', [entry['title']]): query = normalize_unicode(search_string) query = re.sub(r' \(\d\d\d\d\)$', '', query) log.debug('Searching Descargas2020 %s', query) query = unicodedata.normalize('NFD', query).encode('ascii', 'ignore') data = {'q': query} try: response = task.requests.post(url_search, data=data) except requests.RequestException as e: log.error('Error searching Descargas2020: %s', e) return results content = response.content soup = get_soup(content) soup2 = soup.find('ul', attrs={'class': 'buscar-list'}) children = soup2.findAll('a', href=True) for child in children: entry = Entry() entry['url'] = child['href'] entry_title = child.find('h2') if entry_title is None: log.debug('Ignore empty entry') continue entry_title = entry_title.text if not entry_title: continue try: entry_quality_lan = re.search( r'.+ \[([^\]]+)\](\[[^\]]+\])+$', entry_title).group(1) except AttributeError: log.debug('Quality not found') continue entry_title = re.sub(r' \[.+]$', '', entry_title) entry['title'] = entry_title + ' ' + entry_quality_lan results.add(entry) log.debug('Finish search Descargas2020 with %d entries', len(results)) return results
class InputWhatCD(object): """A plugin that searches what.cd == Usage: All parameters except `username` and `password` are optional. whatcd: username: password: user_agent: (A custom user-agent for the client to report. It is NOT A GOOD IDEA to spoof a browser with this. You are responsible for your account.) search: (general search filter) artist: (artist name) album: (album name) year: (album year) encoding: (encoding specifics - 192, 320, lossless, etc.) format: (MP3, FLAC, AAC, etc.) media: (CD, DVD, vinyl, Blu-ray, etc.) release_type: (album, soundtrack, EP, etc.) log: (log specification - true, false, '100%', or '<100%') hascue: (has a cue file - true or false) scene: (is a scene release - true or false) vanityhouse: (is a vanity house release - true or false) leech_type: ('freeleech', 'neutral', 'either', or 'normal') tags: (a list of tags to match - drum.and.bass, new.age, blues, etc.) tag_type: (match 'any' or 'all' of the items in `tags`) """ # Aliases for config -> api params ALIASES = { "artist": "artistname", "album": "groupname", "leech_type": "freetorrent", "release_type": "releaseType", "tags": "taglist", "tag_type": "tags_type", "search": "searchstr", "log": "haslog", } # API parameters # None means a raw value entry (no validation) # A dict means a choice with a mapping for the API # A list is just a choice with no mapping PARAMS = { "searchstr": None, "taglist": None, "artistname": None, "groupname": None, "year": None, "tags_type": { "any": 0, "all": 1, }, "encoding": [ "192", "APS (VBR)", "V2 (VBR)", "V1 (VBR)", "256", "APX (VBR)", "V0 (VBR)", "320", "lossless", "24bit lossless", "V8 (VBR)" ], "format": [ "MP3", "FLAC", "AAC", "AC3", "DTS" ], "media": [ "CD", "DVD", "vinyl", "soundboard", "SACD", "DAT", "cassette", "WEB", "Blu-ray" ], "releaseType": { "album": 1, "soundtrack": 3, "EP": 5, "anthology": 6, "compilation": 7, "DJ mix": 8, "single": 9, "live album": 11, "remix": 13, "bootleg": 14, "interview": 15, "mixtape": 16, "unknown": 21, "concert recording": 22, "demo": 23 }, "haslog": { "False": 0, "True": 1, "100%": 100, "<100%": -1 }, "freetorrent": { "freeleech": 1, "neutral": 2, "either": 3, "normal": 0, }, "hascue": { "False": 0, "True": 1, }, "scene": { "False": 0, "True": 1, }, "vanityhouse": { "False": 0, "True": 1, } } def _key(self, key): """Gets the API key name from the entered key""" if key in self.ALIASES: return self.ALIASES[key] return key def _opts(self, key): """Gets the options for the specified key""" return self.PARAMS[self._key(key)] def _getval(self, key, val): """Gets the value for the specified key based on a config option""" opts = self._opts(key) if isinstance(opts, dict): # Translate the input value to the What.CD API value # The str cast converts bools to 'True'/'False' for use as keys # This allows for options that have True/False/Other values return opts[str(val)] elif isinstance(val, list): # Fix yaml parser making a list out of a string return ",".join(val) return val def __init__(self): """Set up the schema""" self.schema = { 'type': 'object', 'properties': { 'username': {'type': 'string'}, 'password': {'type': 'string'}, 'user_agent': {'type': 'string'}, 'search': {'type': 'string'}, 'artist': {'type': 'string'}, 'album': {'type': 'string'}, 'year': {'type': ['string', 'integer']}, 'tags': one_or_more({'type': 'string'}), 'tag_type': {'type': 'string', 'enum': list(self._opts('tag_type').keys())}, 'encoding': {'type': 'string', 'enum': self._opts('encoding')}, 'format': {'type': 'string', 'enum': self._opts('format')}, 'media': {'type': 'string', 'enum': self._opts('media')}, 'release_type': {'type': 'string', 'enum': list(self._opts('release_type').keys())}, 'log': {'oneOf': [{'type': 'string', 'enum': list(self._opts('log').keys())}, {'type': 'boolean'}]}, 'leech_type': {'type': 'string', 'enum': list(self._opts('leech_type').keys())}, 'hascue': {'type': 'boolean'}, 'scene': {'type': 'boolean'}, 'vanityhouse': {'type': 'boolean'}, }, 'required': ['username', 'password'], 'additionalProperties': False } def _login(self, user, passwd): """ Log in and store auth data from the server Adapted from https://github.com/isaaczafuta/whatapi """ data = { 'username': user, 'password': passwd, 'keeplogged': 1, } r = self.session.post("https://ssl.what.cd/login.php", data=data, allow_redirects=False) if r.status_code != 302 or r.headers.get('location') != "index.php": raise PluginError("Failed to log in to What.cd") accountinfo = self._request('index') self.authkey = accountinfo['authkey'] self.passkey = accountinfo['passkey'] log.info("Logged in to What.cd") def _request(self, action, page=None, **kwargs): """ Make an AJAX request to a given action page Adapted from https://github.com/isaaczafuta/whatapi """ ajaxpage = "https://ssl.what.cd/ajax.php" params = {} # Filter params and map config values -> api values for k, v in list(kwargs.items()): params[self._key(k)] = self._getval(k, v) # Params other than the searching ones params['action'] = action if page: params['page'] = page r = self.session.get(ajaxpage, params=params, allow_redirects=False) if r.status_code != 200: raise PluginError("What.cd returned a non-200 status code") try: json_response = r.json() if json_response['status'] != "success": # Try to deal with errors returned by the API error = json_response.get('error', json_response.get('status')) if not error or error == "failure": error = json_response.get('response', str(json_response)) raise PluginError("What.cd gave a failure response: " "'{}'".format(error)) return json_response['response'] except (ValueError, TypeError, KeyError) as e: raise PluginError("What.cd returned an invalid response") def _search_results(self, config): """Generator that yields search results""" page = 1 pages = None while True: if pages and page >= pages: break log.debug("Attempting to get page {} of search results".format(page)) result = self._request('browse', page=page, **config) if not result['results']: break for x in result['results']: yield x pages = result.get('pages', pages) page += 1 def _get_entries(self, search_results): """Genertor that yields Entry objects from search results""" for result in search_results: # Get basic information on the release info = dict((k, result[k]) for k in ('artist', 'groupName', 'groupYear')) # Releases can have multiple download options for tor in result['torrents']: temp = info.copy() temp.update(dict((k, tor[k]) for k in ('media', 'encoding', 'format', 'torrentId'))) yield Entry( title="{artist} - {groupName} - {groupYear} " "({media} - {format} - {encoding})-{torrentId}.torrent".format(**temp), url="https://what.cd/torrents.php?action=download&" "id={}&authkey={}&torrent_pass={}".format(temp['torrentId'], self.authkey, self.passkey), torrent_seeds=tor['seeders'], torrent_leeches=tor['leechers'], # Size is returned in bytes, convert to MB for compat with the content_size plugin content_size=math.floor(tor['size'] / (1024**2)) ) @cached('whatcd') @plugin.internet(log) def on_task_input(self, task, config): """Search on What.cd""" self.session = Session() # From the API docs: "Refrain from making more than five (5) requests every ten (10) seconds" self.session.add_domain_limiter(TokenBucketLimiter('ssl.what.cd', 2, '2 seconds')) # Custom user agent user_agent = config.pop('user_agent', None) if user_agent: self.session.headers.update({"User-Agent": user_agent}) # Login self._login(config.pop('username'), config.pop('password')) # Logged in successfully, it's ok if nothing matches task.no_entries_ok = True # NOTE: Any values still in config at this point MUST be valid search parameters # Perform the search and parse the needed information out of the response results = self._search_results(config) return list(self._get_entries(results))
from future.moves.urllib.parse import parse_qs, urlparse import re import logging from flexget import plugin from flexget.event import event from flexget.plugins.plugin_urlrewriting import UrlRewritingError from flexget.utils.requests import Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger('google') requests = Session() requests.headers.update({'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}) requests.add_domain_limiter(TimedLimiter('imdb.com', '2 seconds')) class UrlRewriteGoogleCse(object): """Google custom query urlrewriter.""" # urlrewriter API def url_rewritable(self, task, entry): if entry['url'].startswith('http://www.google.com/cse?'): return True if entry['url'].startswith('http://www.google.com/custom?'): return True return False # urlrewriter API def url_rewrite(self, task, entry):
class UrlRewriteDescargas2020(object): """Descargas2020 urlrewriter and search.""" schema = {'type': 'boolean', 'default': False} def __init__(self): self._session = None @property def session(self): # TODO: This is not used for all requests even .. if self._session is None: self._session = Session() self._session.headers.update( {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} ) self._session.add_domain_limiter(TimedLimiter('descargas2020.com', '2 seconds')) return self._session # urlrewriter API def url_rewritable(self, task, entry): url = entry['url'] return REWRITABLE_REGEX.match(url) and not NONREWRITABLE_REGEX.match(url) # urlrewriter API def url_rewrite(self, task, entry): entry['url'] = self.parse_download_page(entry['url'], task) @plugin.internet(log) def parse_download_page(self, url, task): log.verbose('Descargas2020 URL: %s', url) try: page = self.session.get(url) except requests.RequestException as e: raise UrlRewritingError(e) try: soup = get_soup(page.text) except Exception as e: raise UrlRewritingError(e) torrent_id = None url_format = DESCARGAS2020_TORRENT_FORMAT torrent_id_prog = re.compile( r"(?:parametros\s*=\s*\n?)\s*{\s*\n(?:\s*'\w+'\s*:.*\n)+\s*'(?:torrentID|id)'\s*:\s*'(\d+)'" ) torrent_ids = soup.findAll(text=torrent_id_prog) if torrent_ids: match = torrent_id_prog.search(torrent_ids[0]) if match: torrent_id = match.group(1) if not torrent_id: log.debug('torrent ID not found, searching openTorrent script') match = re.search( r'function openTorrent.*\n.*\{.*(\n.*)+window\.location\.href =\s*\".*\/(\d+.*)\";', page.text, re.MULTILINE, ) if match: torrent_id = match.group(2).rstrip('/') if not torrent_id: raise UrlRewritingError('Unable to locate torrent ID from url %s' % url) return url_format.format(torrent_id) def search(self, task, entry, config=None): if not config: log.debug('Descargas2020 disabled') return set() log.debug('Search Descargas2020') url_search = 'http://descargas2020.com/buscar' results = set() for search_string in entry.get('search_strings', [entry['title']]): query = normalize_unicode(search_string) query = re.sub(r' \(\d\d\d\d\)$', '', query) log.debug('Searching Descargas2020 %s', query) query = unicodedata.normalize('NFD', query).encode('ascii', 'ignore') data = {'q': query} try: response = task.requests.post(url_search, data=data) except requests.RequestException as e: log.error('Error searching Descargas2020: %s', e) return results content = response.content soup = get_soup(content) soup2 = soup.find('ul', attrs={'class': 'buscar-list'}) children = soup2.findAll('a', href=True) for child in children: entry = Entry() entry['url'] = child['href'] entry_title = child.find('h2') if entry_title is None: log.debug('Ignore empty entry') continue entry_title = entry_title.text if not entry_title: continue try: entry_quality_lan = re.search( r'.+ \[([^\]]+)\](\[[^\]]+\])+$', entry_title ).group(1) except AttributeError: log.debug('Quality not found') continue entry_title = re.sub(r' \[.+]$', '', entry_title) entry['title'] = entry_title + ' ' + entry_quality_lan results.add(entry) log.debug('Finish search Descargas2020 with %d entries', len(results)) return results
from requests.exceptions import RequestException from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession from flexget.utils.requests import TimedLimiter plugin_name = 'prowl' log = logging.getLogger(plugin_name) PROWL_URL = 'https://api.prowlapp.com/publicapi/add' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('prowlapp.com', '5 seconds')) class ProwlNotifier: """ Send prowl notifications Example:: notify: entries: via: - prowl: api_key: xxxxxxx [application: application name, default FlexGet] [event: event title, default New Release]
from future.moves.urllib.parse import parse_qs, urlparse import re import logging from flexget import plugin from flexget.event import event from flexget.plugins.plugin_urlrewriting import UrlRewritingError from flexget.utils.requests import Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger('google') requests = Session() requests.headers.update({'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}) requests.add_domain_limiter(TimedLimiter('imdb.com', '2 seconds')) class UrlRewriteGoogleCse(object): """Google custom query urlrewriter.""" # urlrewriter API def url_rewritable(self, task, entry): if entry['url'].startswith('http://www.google.com/cse?'): return True if entry['url'].startswith('http://www.google.com/custom?'): return True return False # urlrewriter API def url_rewrite(self, task, entry):
install_aliases() import logging from urllib.parse import parse_qs, urlparse from bs4 import BeautifulSoup from flexget import plugin from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.utils.requests import Session as RequestSession log = logging.getLogger('brokenstones_lookup') requests = RequestSession() requests.add_domain_limiter(TimedLimiter('brokenstones.club', '10 seconds')) def login(username, password): log.info('Logging in to BrokenStones...') data = {'username': username, 'password': password, 'keeplogged': '1'} try: login_url = 'https://brokenstones.club/login.php' r = requests.post(login_url, data=data) if r.url == login_url: raise plugin.PluginError('Failed to log in to BrokenStones') except RequestException as e: raise plugin.PluginError( 'Error logging in to BrokenStones: {}'.format(e))
from loguru import logger from requests.exceptions import RequestException from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession from flexget.utils.requests import TimedLimiter plugin_name = 'join' logger = logger.bind(name=plugin_name) requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('appspot.com', '5 seconds')) JOIN_URL = 'https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush' class JoinNotifier: """ Example:: notify: entries: via: - join: [api_key: <API_KEY> (your join api key. Only required for 'group' notifications)] [group: <GROUP_NAME> (name of group of join devices to notify. 'all', 'android', etc.) [device: <DEVICE_ID> (can also be a list of device ids)] [url: <NOTIFICATION_URL>]
from loguru import logger from flexget import plugin from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'sms_free_fr' logger = logger.bind(name=plugin_name) SMS_SEND_URL = 'https://smsapi.free-mobile.fr/sendmsg' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('smsapi.free-mobile.fr', '5 seconds')) class SMSFreeFrNotifier(object): """ Sends SMS notification through smsapi.free-mobile.fr Informations: https://www.freenews.fr/freenews-edition-nationale-299/free-mobile-170/nouvelle-option-notifications-par-sms-chez-free-mobile-14817 Example: sms_free_fr: user: your login (accepted format example: '12345678') password: <PASSWORD>
from flexget import plugin from flexget.event import event from flexget.plugins.internal.urlrewriting import UrlRewritingError from flexget.utils.requests import Session, TimedLimiter from flexget.utils.soup import get_soup from flexget.entry import Entry from flexget.utils.search import normalize_unicode from bs4.element import Tag log = logging.getLogger('newpct') requests = Session() requests.headers.update({'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}) requests.add_domain_limiter(TimedLimiter('newpct1.com', '2 seconds')) requests.add_domain_limiter(TimedLimiter('newpct.com', '2 seconds')) NEWPCT_TORRENT_FORMAT = 'http://www.newpct.com/torrents/{:0>6}.torrent' NEWPCT1_TORRENT_FORMAT = 'http://www.newpct1.com/download/{:0>6}.torrent' class UrlRewriteNewPCT(object): """NewPCT urlrewriter and search.""" schema = { 'type': 'boolean', 'default': False } # urlrewriter API
from flexget.utils.soup import get_soup from slugify import slugify from .anidb_cache import cached_anidb, ANIDB_CACHE PLUGIN_ID = 'fadbs.util.anidb' CLIENT_STR = 'fadbs' CLIENT_VER = 1 log = logging.getLogger(PLUGIN_ID) requests = Session() requests.headers.update({'User-Agent': 'Python-urllib/2.6'}) requests.add_domain_limiter(TimedLimiter('api.anidb.net', '2 seconds')) class AnidbSearch(object): """ Search for an anime's id """ anidb_xml_url = 'http://api.anidb.net:9001/httpapi?request=anime' prelook_url = 'http://anisearch.outrance.pl?task=search' cdata_regex = re.compile(r'.+CDATA\[(.+)\]\].+') particle_words = { 'x-jat': {'no', 'wo', 'o', 'na', 'ja', 'ni', 'to', 'ga', 'wa'} } def __init__(self): self.debug = False
import logging from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = "pushalot" log = logging.getLogger(__name__) PUSHALOT_URL = "https://pushalot.com/api/sendmessage" requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter("pushalot.com", "5 seconds")) class PushalotNotifier(object): """ Example:: pushalot: token: <string> Authorization token (can also be a list of tokens) - Required title: <string> (default: task name) body: <string> (default: '{{series_name}} {{series_id}}' ) link: <string> (default: '{{imdb_url}}') linktitle: <string> (default: (none)) important: <boolean> (default is False) silent: <boolean< (default is False) image: <string> (default: (none))
class ImdbEntrySet(MutableSet): schema = { 'type': 'object', 'properties': { 'login': {'type': 'string'}, 'password': {'type': 'string'}, 'list': {'type': 'string'}, 'force_language': {'type': 'string', 'default': 'en-us'} }, 'additionalProperties': False, 'required': ['login', 'password', 'list'] } def __init__(self, config): self.config = config self._session = RequestSession() self._session.add_domain_limiter(TimedLimiter('imdb.com', '5 seconds')) self._session.headers.update({'Accept-Language': config.get('force_language', 'en-us')}) self.user_id = None self.list_id = None self.cookies = None self.hidden_value = None self._items = None self._authenticated = False @property def session(self): if not self._authenticated: self.authenticate() return self._session def get_user_id_and_hidden_value(self, cookies=None): try: if cookies: self._session.cookies = cookiejar_from_dict(cookies) # We need to allow for redirects here as it performs 1-2 redirects before reaching the real profile url response = self._session.get('https://www.imdb.com/profile', allow_redirects=True) except RequestException as e: raise PluginError(str(e)) user_id_match = re.search('ur\d+(?!\d)', response.url) if user_id_match: # extract the hidden form value that we need to do post requests later on try: soup = get_soup(response.text) self.hidden_value = soup.find('input', attrs={'id': '49e6c'})['value'] except Exception as e: log.warning('Unable to locate the hidden form value ''49e6c''. Without it, you might not be able to ' 'add or remove items. %s', e) return user_id_match.group() if user_id_match else None def authenticate(self): """Authenticates a session with IMDB, and grabs any IDs needed for getting/modifying list.""" cached_credentials = False with Session() as session: user = session.query(IMDBListUser).filter(IMDBListUser.user_name == self.config.get('login')).one_or_none() if user and user.cookies and user.user_id: log.debug('login credentials found in cache, testing') self.user_id = user.user_id if not self.get_user_id_and_hidden_value(cookies=user.cookies): log.debug('cache credentials expired') user.cookies = None self._session.cookies.clear() else: self.cookies = user.cookies cached_credentials = True if not cached_credentials: log.debug('user credentials not found in cache or outdated, fetching from IMDB') url_credentials = ( 'https://www.imdb.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.imdb.com%2Fap-signin-' 'handler&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&' 'openid.assoc_handle=imdb_mobile_us&openid.mode=checkid_setup&openid.claimed_id=http%3A%' '2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.ope' 'nid.net%2Fauth%2F2.0' ) try: # we need to get some cookies first self._session.get('https://www.imdb.com') r = self._session.get(url_credentials) except RequestException as e: raise PluginError(e.args[0]) soup = get_soup(r.content) form = soup.find('form', attrs={'name': 'signIn'}) inputs = form.select('input') data = dict((i['name'], i.get('value')) for i in inputs if i.get('name')) data['email'] = self.config['login'] data['password'] = self.config['password'] action = form.get('action') log.debug('email=%s, password=%s', data['email'], data['password']) self._session.headers.update({'Referer': url_credentials}) self._session.post(action, data=data) self._session.headers.update({'Referer': 'https://www.imdb.com/'}) self.user_id = self.get_user_id_and_hidden_value() if not self.user_id: raise plugin.PluginError('Login to IMDB failed. Check your credentials.') self.cookies = self._session.cookies.get_dict(domain='.imdb.com') # Get list ID if user: for list in user.lists: if self.config['list'] == list.list_name: log.debug('found list ID %s matching list name %s in cache', list.list_id, list.list_name) self.list_id = list.list_id if not self.list_id: log.debug('could not find list ID in cache, fetching from IMDB') if self.config['list'] == 'watchlist': data = {'consts[]': 'tt0133093', 'tracking_tag': 'watchlistRibbon'} wl_data = self._session.post('https://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() try: self.list_id = wl_data['list_id'] except KeyError: raise PluginError('No list ID could be received. Please initialize list by ' 'manually adding an item to it and try again') elif self.config['list'] in IMMUTABLE_LISTS or self.config['list'].startswith('ls'): self.list_id = self.config['list'] else: data = {'tconst': 'tt0133093'} list_data = self._session.post('https://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for li in list_data['items']: if li['wlb_text'] == self.config['list']: self.list_id = li['data_list_id'] break else: raise plugin.PluginError('Could not find list %s' % self.config['list']) user = IMDBListUser(self.config['login'], self.user_id, self.cookies) list = IMDBListList(self.list_id, self.config['list'], self.user_id) user.lists.append(list) session.merge(user) self._authenticated = True def invalidate_cache(self): self._items = None @property def items(self): if self._items is None: log.debug('fetching items from IMDB') try: r = self.session.get('https://www.imdb.com/list/export?list_id=%s&author_id=%s' % (self.list_id, self.user_id), cookies=self.cookies) lines = list(r.iter_lines(decode_unicode=True)) except RequestException as e: raise PluginError(e.args[0]) # Normalize headers to lowercase lines[0] = lines[0].lower() self._items = [] for row in csv_dictreader(lines): log.debug('parsing line from csv: %s', row) try: item_type = row['title type'].lower() name = row['title'] year = int(row['year']) if row['year'] != '????' else None created = datetime.strptime(row['created'], '%Y-%m-%d') if row.get('created') else None modified = datetime.strptime(row['modified'], '%Y-%m-%d') if row.get('modified') else None entry = Entry({ 'title': '%s (%s)' % (name, year) if year != '????' else name, 'url': row['url'], 'imdb_id': row['const'], 'imdb_url': row['url'], 'imdb_list_position': int(row['position']) if 'position' in row else None, 'imdb_list_created': created, 'imdb_list_modified': modified, 'imdb_list_description': row.get('description'), 'imdb_name': name, 'imdb_year': year, 'imdb_user_score': float(row['imdb rating']) if row['imdb rating'] else None, 'imdb_votes': int(row['num votes']) if row['num votes'] else None, 'imdb_genres': [genre.strip() for genre in row['genres'].split(',')] }) except ValueError as e: log.debug('no movie row detected, skipping. %s. Exception: %s', row, e) continue if item_type in MOVIE_TYPES: entry['movie_name'] = name entry['movie_year'] = year elif item_type in SERIES_TYPES: entry['series_name'] = name entry['series_year'] = year elif item_type in OTHER_TYPES: entry['title'] = name else: log.verbose('Unknown IMDB type entry received: %s. Skipping', item_type) continue self._items.append(entry) return self._items @property def immutable(self): if self.config['list'] in IMMUTABLE_LISTS: return '%s list is not modifiable' % self.config['list'] def _from_iterable(cls, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it) def __contains__(self, entry): return self.get(entry) is not None def __iter__(self): return iter(self.items) def discard(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot remove %s from imdb_list because it does not have an imdb_id', entry['title']) return # Get the list item id item_ids = None urls = [] if self.config['list'] == 'watchlist': method = 'delete' data = {'consts[]': entry['imdb_id'], 'tracking_tag': 'watchlistRibbon'} status = self.session.post('https://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() item_ids = status.get('has', {}).get(entry['imdb_id']) urls = ['https://www.imdb.com/watchlist/%s' % entry['imdb_id']] else: method = 'post' data = {'tconst': entry['imdb_id']} status = self.session.post('https://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for a_list in status['items']: if a_list['data_list_id'] == self.list_id: item_ids = a_list['data_list_item_ids'] break for item_id in item_ids: urls.append('https://www.imdb.com/list/%s/li%s/delete' % (self.list_id, item_id)) if not item_ids: log.warning('%s is not in list %s, cannot be removed', entry['imdb_id'], self.list_id) return for url in urls: log.debug('found movie %s with ID %s in list %s, removing', entry['title'], entry['imdb_id'], self.list_id) self.session.request(method, url, data={'49e6c': self.hidden_value}, cookies=self.cookies) # We don't need to invalidate our cache if we remove the item self._items = [i for i in self._items if i['imdb_id'] != entry['imdb_id']] if self._items else None def _add(self, entry): """Submit a new movie to imdb. (does not update cache)""" if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot add %s to imdb_list because it does not have an imdb_id', entry['title']) return # Manually calling authenticate to fetch list_id and cookies and hidden form value self.authenticate() if self.config['list'] == 'watchlist': method = 'put' url = 'https://www.imdb.com/watchlist/%s' % entry['imdb_id'] else: method = 'post' url = 'https://www.imdb.com/list/%s/%s/add' % (self.list_id, entry['imdb_id']) log.debug('adding title %s with ID %s to imdb %s', entry['title'], entry['imdb_id'], self.list_id) self.session.request(method, url, cookies=self.cookies, data={'49e6c': self.hidden_value}) def add(self, entry): self._add(entry) # Invalidate the cache so that we get the canonical entry from the imdb list self.invalidate_cache() def __ior__(self, entries): for entry in entries: self._add(entry) self.invalidate_cache() return self def __len__(self): return len(self.items) @property def online(self): """ Set the online status of the plugin, online plugin should be treated differently in certain situations, like test mode""" return True def get(self, entry): if not entry.get('imdb_id'): log.debug('entry %s does not have imdb_id, cannot compare to imdb list items', entry) return None log.debug('finding %s in imdb list', entry['imdb_id']) for e in self.items: if e['imdb_id'] == entry['imdb_id']: return e log.debug('could not find %s in imdb list items', entry['imdb_id']) return None
from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import Session as RequestSession, TimedLimiter from flexget.utils.soup import get_soup from requests.exceptions import HTTPError, RequestException from datetime import datetime, date, timedelta import unicodedata import re log = logging.getLogger('search_npo') requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('npostart.nl', '8 seconds')) class NPOWatchlist(object): """ Produces entries for every episode on the user's npostart.nl watchlist (Dutch public television). Entries can be downloaded using http://arp242.net/code/download-npo If 'remove_accepted' is set to 'yes', the plugin will delete accepted entries from the watchlist after download is complete. If 'max_episode_age_days' is set (and not 0), entries will only be generated for episodes broadcast in the last x days. This only applies to episodes related to series the user is following. For example: npo_watchlist: email: [email protected]
import datetime from flexget import plugin from flexget.event import event from flexget.config_schema import one_or_more from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'pushbullet' log = logging.getLogger(plugin_name) PUSHBULLET_URL = 'https://api.pushbullet.com/v2/pushes' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushbullet.com', '5 seconds')) class PushbulletNotifier(object): """ Example:: notify: entries: via: pushbullet: apikey: <API_KEY> [device: <DEVICE_IDEN> (can also be a list of device ids, or don't specify any ids to send to all devices)] [email: <EMAIL_ADDRESS> (can also be a list of user email addresses)] [channel: <CHANNEL_TAG> (you can only specify device / email or channel tag, cannot use both)]
import hashlib from flexget import plugin from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'sms_ru' log = logging.getLogger(__name__) SMS_SEND_URL = 'http://sms.ru/sms/send' SMS_TOKEN_URL = 'http://sms.ru/auth/get_token' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('sms.ru', '5 seconds')) class SMSRuNotifier(object): """ Sends SMS notification through sms.ru http api sms/send. Phone number is a login assigned to sms.ru account. Example: sms_ru: phone_number: <PHONE_NUMBER> (accepted format example: '79997776655') password: <PASSWORD> """ schema = {
from datetime import datetime from dateutil.parser import ParserError, isoparse from loguru import logger from flexget import plugin from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import RequestException, Session, TimedLimiter plugin_name = 'discord' logger = logger.bind(name=plugin_name) session = Session() session.add_domain_limiter(TimedLimiter('discord.com', '3 seconds')) class DiscordNotifier: """ Example:: notify: entries: via: - discord: web_hook_url: <string> [username: <string>] (override the default username of the webhook) [avatar_url: <string>] (override the default avatar of the webhook) [embeds: <arrays>[<object>]] (override embeds) """
import logging from flexget import plugin from flexget.event import event from flexget.config_schema import one_or_more from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'rapidpush' log = logging.getLogger(__name__) RAPIDPUSH_URL = 'https://rapidpush.net/api' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('rapidpush.net', '5 seconds')) class RapidpushNotifier(object): """ Example:: rapidpush: apikey: xxxxxxx (can also be a list of api keys) [category: category, default FlexGet] [group: device group, default no group] [channel: the broadcast notification channel, if provided it will be send to the channel subscribers instead of your devices, default no channel] [priority: 0 - 6 (6 = highest), default 2 (normal)] """ schema = {
import logging from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'pushover' log = logging.getLogger(__name__) PUSHOVER_URL = 'https://api.pushover.net/1/messages.json' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushover.net', '5 seconds')) class PushoverNotifier(object): """ Example:: pushover: user_key: <USER_KEY> (can also be a list of userkeys) token: <TOKEN> [device: <DEVICE_STRING>] [title: <MESSAGE_TITLE>] [message: <MESSAGE_BODY>] [priority: <PRIORITY>] [url: <URL>] [url_title: <URL_TITLE>]
from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import Session as RequestSession, TimedLimiter from flexget.utils.soup import get_soup from requests.exceptions import HTTPError, RequestException from datetime import date, timedelta import unicodedata import re log = logging.getLogger('search_npo') requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('npo.nl', '5 seconds')) fragment_regex = re.compile('[A-Z][^/]+/') date_regex = re.compile('([1-3]?[0-9]) ([a-z]{3})(?: ([0-9]{4})|\W)') days_ago_regex = re.compile('([0-9]+) dagen geleden') months = ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'] class NPOWatchlist(object): """ Produces entries for every episode on the user's npo.nl watchlist (Dutch public television). Entries can be downloaded using http://arp242.net/code/download-npo If 'remove_accepted' is set to 'yes', the plugin will delete accepted entries from the watchlist after download is complete. If 'max_episode_age_days' is set (and not 0), entries will only be generated for episodes broadcast in the last
import logging from flexget import plugin from flexget.event import event from flexget.config_schema import one_or_more from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'rapidpush' log = logging.getLogger(__name__) RAPIDPUSH_URL = 'https://rapidpush.net/api' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('rapidpush.net', '5 seconds')) class RapidpushNotifier(object): """ Example:: rapidpush: apikey: xxxxxxx (can also be a list of api keys) [category: category, default FlexGet] [group: device group, default no group] [channel: the broadcast notification channel, if provided it will be send to the channel subscribers instead of your devices, default no channel] [priority: 0 - 6 (6 = highest), default 2 (normal)] """ schema = {
from dateutil.parser import parse as dateutil_parse from flexget import plugin, db_schema from flexget.config_schema import one_or_more from flexget.entry import Entry from flexget.event import event from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession, TimedLimiter, RequestException from flexget.utils.tools import parse_filesize log = logging.getLogger('passthepopcorn') Base = db_schema.versioned_base('passthepopcorn', 1) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('passthepopcorn.me', '5 seconds')) TAGS = [ 'action', 'adventure', 'animation', 'arthouse', 'asian', 'biography', 'camp', 'comedy', 'crime', 'cult', 'documentary', 'drama', 'experimental', 'exploitation', 'family', 'fantasy', 'film.noir', 'history', 'horror', 'martial.arts', 'musical', 'mystery', 'performance', 'philosophy', 'politics', 'romance', 'sci.fi', 'short', 'silent', 'sport', 'thriller', 'video.art', 'war', 'western' ] ORDERING = { 'Relevance': 'relevance', 'Time added': 'timeadded', 'Time w/o reseed': 'timenoreseed', 'First time added': 'creationtime',
class ImdbEntrySet(MutableSet): schema = { 'type': 'object', 'properties': { 'login': {'type': 'string'}, 'password': {'type': 'string'}, 'list': {'type': 'string'}, 'force_language': {'type': 'string', 'default': 'en-us'} }, 'additionalProperties': False, 'required': ['login', 'password', 'list'] } def __init__(self, config): self.config = config self._session = RequestSession() self._session.add_domain_limiter(TimedLimiter('imdb.com', '5 seconds')) self._session.headers.update({'Accept-Language': config.get('force_language', 'en-us')}) self.user_id = None self.list_id = None self.cookies = None self._items = None self._authenticated = False @property def session(self): if not self._authenticated: self.authenticate() return self._session def authenticate(self): """Authenticates a session with IMDB, and grabs any IDs needed for getting/modifying list.""" cached_credentials = False with Session() as session: user = session.query(IMDBListUser).filter(IMDBListUser.user_name == self.config.get('login')).one_or_none() if user and user.cookies and user.user_id: log.debug('login credentials found in cache, testing') self.cookies = user.cookies self.user_id = user.user_id r = self._session.head('http://www.imdb.com/profile', allow_redirects=False, cookies=self.cookies) if not r.headers.get('location') or 'login' in r.headers['location']: log.debug('cache credentials expired') else: cached_credentials = True if not cached_credentials: log.debug('user credentials not found in cache or outdated, fetching from IMDB') url_credentials = ( 'https://www.imdb.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.imdb.com%2Fap-signin-' 'handler&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&' 'openid.assoc_handle=imdb_mobile_us&openid.mode=checkid_setup&openid.claimed_id=http%3A%' '2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.ope' 'nid.net%2Fauth%2F2.0' ) try: r = self._session.get(url_credentials) except RequestException as e: raise PluginError(e.args[0]) soup = get_soup(r.content) inputs = soup.select('form#ap_signin_form input') data = dict((i['name'], i.get('value')) for i in inputs if i.get('name')) data['email'] = self.config['login'] data['password'] = self.config['password'] action = soup.find('form', id='ap_signin_form').get('action') log.debug('email=%s, password=%s', data['email'], data['password']) self._session.headers.update({'Referer': url_credentials}) d = self._session.post(action, data=data) self._session.headers.update({'Referer': 'http://www.imdb.com/'}) # Get user id by extracting from redirect url r = self._session.head('http://www.imdb.com/profile', allow_redirects=False) if not r.headers.get('location') or 'login' in r.headers['location']: raise plugin.PluginError('Login to IMDB failed. Check your credentials.') self.user_id = re.search('ur\d+(?!\d)', r.headers['location']).group() self.cookies = dict(d.cookies) # Get list ID if user: for list in user.lists: if self.config['list'] == list.list_name: log.debug('found list ID %s matching list name %s in cache', list.list_id, list.list_name) self.list_id = list.list_id if not self.list_id: log.debug('could not find list ID in cache, fetching from IMDB') if self.config['list'] == 'watchlist': data = {'consts[]': 'tt0133093', 'tracking_tag': 'watchlistRibbon'} wl_data = self._session.post('http://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() try: self.list_id = wl_data['list_id'] except KeyError: raise PluginError('No list ID could be received. Please initialize list by ' 'manually adding an item to it and try again') elif self.config['list'] in IMMUTABLE_LISTS or self.config['list'].startswith('ls'): self.list_id = self.config['list'] else: data = {'tconst': 'tt0133093'} list_data = self._session.post('http://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for li in list_data['items']: if li['wlb_text'] == self.config['list']: self.list_id = li['data_list_id'] break else: raise plugin.PluginError('Could not find list %s' % self.config['list']) user = IMDBListUser(self.config['login'], self.user_id, self.cookies) list = IMDBListList(self.list_id, self.config['list'], self.user_id) user.lists.append(list) session.merge(user) self._authenticated = True def invalidate_cache(self): self._items = None @property def items(self): if self._items is None: log.debug('fetching items from IMDB') try: r = self.session.get('http://www.imdb.com/list/export?list_id=%s&author_id=%s' % (self.list_id, self.user_id), cookies=self.cookies) except RequestException as e: raise PluginError(e.args[0]) lines = r.iter_lines(decode_unicode=True) # Throw away first line with headers next(lines) self._items = [] for row in csv_reader(lines): log.debug('parsing line from csv: %s', ', '.join(row)) if not len(row) == 16: log.debug('no movie row detected, skipping. %s', ', '.join(row)) continue entry = Entry({ 'title': '%s (%s)' % (row[5], row[11]) if row[11] != '????' else '%s' % row[5], 'url': row[15], 'imdb_id': row[1], 'imdb_url': row[15], 'imdb_list_position': int(row[0]), 'imdb_list_created': datetime.strptime(row[2], '%a %b %d %H:%M:%S %Y') if row[2] else None, 'imdb_list_modified': datetime.strptime(row[3], '%a %b %d %H:%M:%S %Y') if row[3] else None, 'imdb_list_description': row[4], 'imdb_name': row[5], 'imdb_year': int(row[11]) if row[11] != '????' else None, 'imdb_score': float(row[9]) if row[9] else None, 'imdb_user_score': float(row[8]) if row[8] else None, 'imdb_votes': int(row[13]) if row[13] else None, 'imdb_genres': [genre.strip() for genre in row[12].split(',')] }) item_type = row[6].lower() name = row[5] year = int(row[11]) if row[11] != '????' else None if item_type in MOVIE_TYPES: entry['movie_name'] = name entry['movie_year'] = year elif item_type in SERIES_TYPES: entry['series_name'] = name entry['series_year'] = year elif item_type in OTHER_TYPES: entry['title'] = name else: log.verbose('Unknown IMDB type entry received: %s. Skipping', item_type) continue self._items.append(entry) return self._items @property def immutable(self): if self.config['list'] in IMMUTABLE_LISTS: return '%s list is not modifiable' % self.config['list'] def _from_iterable(cls, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it) def _find_movie(self, entry): log.debug('trying to match %s to existing list items', entry['imdb_id']) for e in self.items: if e['imdb_id'] == entry['imdb_id']: return e log.debug('could not match %s to existing list items', entry['imdb_id']) def __contains__(self, entry): if not entry.get('imdb_id'): log.debug('entry %s does not have imdb_id, skipping', entry) return False return self._find_movie(entry) is not None def __iter__(self): return iter(self.items) def discard(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot remove %s from imdb_list because it does not have an imdb_id', entry['title']) return # Get the list item id item_ids = None if self.config['list'] == 'watchlist': data = {'consts[]': entry['imdb_id'], 'tracking_tag': 'watchlistRibbon'} status = self.session.post('http://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() item_ids = status.get('has', {}).get(entry['imdb_id']) else: data = {'tconst': entry['imdb_id']} status = self.session.post('http://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for a_list in status['items']: if a_list['data_list_id'] == self.list_id: item_ids = a_list['data_list_item_ids'] break if not item_ids: log.warning('%s is not in list %s, cannot be removed', entry['imdb_id'], self.list_id) return data = { 'action': 'delete', 'list_id': self.list_id, 'ref_tag': 'title' } for item_id in item_ids: log.debug('found movie %s with ID %s in list %s, removing', entry['title'], entry['imdb_id'], self.list_id) self.session.post('http://www.imdb.com/list/_ajax/edit', data=dict(data, list_item_id=item_id), cookies=self.cookies) # We don't need to invalidate our cache if we remove the item self._items = [i for i in self._items if i['imdb_id'] != entry['imdb_id']] if self._items else None def _add(self, entry): """Submit a new movie to imdb. (does not update cache)""" if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot add %s to imdb_list because it does not have an imdb_id', entry['title']) return # Manually calling authenticate to fetch list_id and cookies self.authenticate() data = { 'const': entry['imdb_id'], 'list_id': self.list_id, 'ref_tag': 'title' } log.debug('adding title %s with ID %s to imdb %s', entry['title'], entry['imdb_id'], self.list_id) self.session.post('http://www.imdb.com/list/_ajax/edit', data=data, cookies=self.cookies) def add(self, entry): self._add(entry) # Invalidate the cache so that we get the canonical entry from the imdb list self.invalidate_cache() def __ior__(self, entries): for entry in entries: self._add(entry) self.invalidate_cache() return self def __len__(self): return len(self.items) @property def online(self): """ Set the online status of the plugin, online plugin should be treated differently in certain situations, like test mode""" return True def get(self, entry): return self._find_movie(entry)
from flexget.event import event from flexget.plugins.internal.urlrewriting import UrlRewritingError from flexget.utils.requests import Session, TimedLimiter from flexget.utils.soup import get_soup from flexget.entry import Entry from flexget.utils.search import normalize_unicode import unicodedata log = logging.getLogger('newpct') requests = Session() requests.headers.update( {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}) requests.add_domain_limiter(TimedLimiter('newpct1.com', '2 seconds')) requests.add_domain_limiter(TimedLimiter('newpct.com', '2 seconds')) NEWPCT_TORRENT_FORMAT = 'http://www.newpct.com/torrents/{:0>6}.torrent' NEWPCT1_TORRENT_FORMAT = 'http://www.newpct1.com/download/{}.torrent' class UrlRewriteNewPCT(object): """NewPCT urlrewriter and search.""" schema = {'type': 'boolean', 'default': False} # urlrewriter API def url_rewritable(self, task, entry): url = entry['url'] rewritable_regex = '^http:\/\/(www.)?newpct1?.com\/.*'
from flexget import plugin, db_schema from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession from flexget.utils.soup import get_soup from flexget.config_schema import one_or_more from flexget.utils.tools import parse_filesize log = logging.getLogger('alpharatio') Base = db_schema.versioned_base('alpharatio', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('alpharatio.cc', '5 seconds')) # ElementZero confirmed with AlphaRato sysop 'jasonmaster' that they do want a 5 second limiter CATEGORIES = { 'tvsd': 'filter_cat[1]', 'tvhd': 'filter_cat[2]', 'tvdvdrip': 'filter_cat[3]', 'tvpacksd': 'filter_cat[4]', 'tvpackhd': 'filter_cat[5]', 'moviesd': 'filter_cat[6]', 'moviehd': 'filter_cat[7]', 'moviepacksd': 'filter_cat[8]', 'moviepackhd': 'filter_cat[9]', 'moviexxx': 'filter_cat[10]', 'mvid': 'filter_cat[11]', 'gamespc': 'filter_cat[12]',
import hashlib from flexget import plugin from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'sms_ru' log = logging.getLogger(__name__) SMS_SEND_URL = 'http://sms.ru/sms/send' SMS_TOKEN_URL = 'http://sms.ru/auth/get_token' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('sms.ru', '5 seconds')) class SMSRuNotifier(object): """ Sends SMS notification through sms.ru http api sms/send. Phone number is a login assigned to sms.ru account. Example: sms_ru: phone_number: <PHONE_NUMBER> (accepted format example: '79997776655') password: <PASSWORD> """ schema = {
class ImdbEntrySet(MutableSet): schema = { 'type': 'object', 'properties': { 'login': { 'type': 'string' }, 'password': { 'type': 'string' }, 'list': { 'type': 'string' }, 'force_language': { 'type': 'string', 'default': 'en-us' } }, 'additionalProperties': False, 'required': ['login', 'password', 'list'] } def __init__(self, config): self.config = config self._session = RequestSession() self._session.add_domain_limiter(TimedLimiter('imdb.com', '5 seconds')) self._session.headers.update( {'Accept-Language': config.get('force_language', 'en-us')}) self.user_id = None self.list_id = None self.cookies = None self._items = None self._authenticated = False @property def session(self): if not self._authenticated: self.authenticate() return self._session def authenticate(self): """Authenticates a session with IMDB, and grabs any IDs needed for getting/modifying list.""" cached_credentials = False with Session() as session: user = session.query(IMDBListUser).filter( IMDBListUser.user_name == self.config.get( 'login')).one_or_none() if user and user.cookies and user.user_id: log.debug('login credentials found in cache, testing') self.cookies = user.cookies self.user_id = user.user_id r = self._session.head('http://www.imdb.com/profile', allow_redirects=False, cookies=self.cookies) if not r.headers.get( 'location') or 'login' in r.headers['location']: log.debug('cache credentials expired') else: cached_credentials = True if not cached_credentials: log.debug( 'user credentials not found in cache or outdated, fetching from IMDB' ) url_credentials = ( 'https://www.imdb.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.imdb.com%2Fap-signin-' 'handler&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&' 'openid.assoc_handle=imdb_mobile_us&openid.mode=checkid_setup&openid.claimed_id=http%3A%' '2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.ope' 'nid.net%2Fauth%2F2.0') try: r = self._session.get(url_credentials) except RequestException as e: raise PluginError(e.args[0]) soup = get_soup(r.content) inputs = soup.select('form#ap_signin_form input') data = dict((i['name'], i.get('value')) for i in inputs if i.get('name')) data['email'] = self.config['login'] data['password'] = self.config['password'] action = soup.find('form', id='ap_signin_form').get('action') log.debug('email=%s, password=%s', data['email'], data['password']) self._session.headers.update({'Referer': url_credentials}) d = self._session.post(action, data=data) self._session.headers.update( {'Referer': 'http://www.imdb.com/'}) # Get user id by extracting from redirect url r = self._session.head('http://www.imdb.com/profile', allow_redirects=False) if not r.headers.get( 'location') or 'login' in r.headers['location']: raise plugin.PluginError( 'Login to IMDB failed. Check your credentials.') self.user_id = re.search('ur\d+(?!\d)', r.headers['location']).group() self.cookies = dict(d.cookies) # Get list ID if user: for list in user.lists: if self.config['list'] == list.list_name: log.debug( 'found list ID %s matching list name %s in cache', list.list_id, list.list_name) self.list_id = list.list_id if not self.list_id: log.debug( 'could not find list ID in cache, fetching from IMDB') if self.config['list'] == 'watchlist': data = { 'consts[]': 'tt0133093', 'tracking_tag': 'watchlistRibbon' } wl_data = self._session.post( 'http://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() try: self.list_id = wl_data['list_id'] except KeyError: raise PluginError( 'No list ID could be received. Please initialize list by ' 'manually adding an item to it and try again') elif self.config['list'] in IMMUTABLE_LISTS or self.config[ 'list'].startswith('ls'): self.list_id = self.config['list'] else: data = {'tconst': 'tt0133093'} list_data = self._session.post( 'http://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for li in list_data['items']: if li['wlb_text'] == self.config['list']: self.list_id = li['data_list_id'] break else: raise plugin.PluginError('Could not find list %s' % self.config['list']) user = IMDBListUser(self.config['login'], self.user_id, self.cookies) list = IMDBListList(self.list_id, self.config['list'], self.user_id) user.lists.append(list) session.merge(user) self._authenticated = True def invalidate_cache(self): self._items = None @property def items(self): if self._items is None: log.debug('fetching items from IMDB') try: r = self.session.get( 'http://www.imdb.com/list/export?list_id=%s&author_id=%s' % (self.list_id, self.user_id), cookies=self.cookies) except RequestException as e: raise PluginError(e.args[0]) lines = r.iter_lines(decode_unicode=True) # Throw away first line with headers next(lines) self._items = [] for row in csv_reader(lines): log.debug('parsing line from csv: %s', ', '.join(row)) if not len(row) == 16: log.debug('no movie row detected, skipping. %s', ', '.join(row)) continue entry = Entry({ 'title': '%s (%s)' % (row[5], row[11]) if row[11] != '????' else '%s' % row[5], 'url': row[15], 'imdb_id': row[1], 'imdb_url': row[15], 'imdb_list_position': int(row[0]), 'imdb_list_created': datetime.strptime(row[2], '%a %b %d %H:%M:%S %Y') if row[2] else None, 'imdb_list_modified': datetime.strptime(row[3], '%a %b %d %H:%M:%S %Y') if row[3] else None, 'imdb_list_description': row[4], 'imdb_name': row[5], 'imdb_year': int(row[11]) if row[11] != '????' else None, 'imdb_score': float(row[9]) if row[9] else None, 'imdb_user_score': float(row[8]) if row[8] else None, 'imdb_votes': int(row[13]) if row[13] else None, 'imdb_genres': [genre.strip() for genre in row[12].split(',')] }) item_type = row[6].lower() name = row[5] year = int(row[11]) if row[11] != '????' else None if item_type in MOVIE_TYPES: entry['movie_name'] = name entry['movie_year'] = year elif item_type in SERIES_TYPES: entry['series_name'] = name entry['series_year'] = year elif item_type in OTHER_TYPES: entry['title'] = name else: log.verbose( 'Unknown IMDB type entry received: %s. Skipping', item_type) continue self._items.append(entry) return self._items @property def immutable(self): if self.config['list'] in IMMUTABLE_LISTS: return '%s list is not modifiable' % self.config['list'] def _from_iterable(cls, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it) def _find_movie(self, entry): log.debug('trying to match %s to existing list items', entry['imdb_id']) for e in self.items: if e['imdb_id'] == entry['imdb_id']: return e log.debug('could not match %s to existing list items', entry['imdb_id']) def __contains__(self, entry): if not entry.get('imdb_id'): log.debug('entry %s does not have imdb_id, skipping', entry) return False return self._find_movie(entry) is not None def __iter__(self): return iter(self.items) def discard(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning( 'Cannot remove %s from imdb_list because it does not have an imdb_id', entry['title']) return # Get the list item id item_ids = None if self.config['list'] == 'watchlist': data = { 'consts[]': entry['imdb_id'], 'tracking_tag': 'watchlistRibbon' } status = self.session.post( 'http://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() item_ids = status.get('has', {}).get(entry['imdb_id']) else: data = {'tconst': entry['imdb_id']} status = self.session.post( 'http://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for a_list in status['items']: if a_list['data_list_id'] == self.list_id: item_ids = a_list['data_list_item_ids'] break if not item_ids: log.warning('%s is not in list %s, cannot be removed', entry['imdb_id'], self.list_id) return data = { 'action': 'delete', 'list_id': self.list_id, 'ref_tag': 'title' } for item_id in item_ids: log.debug('found movie %s with ID %s in list %s, removing', entry['title'], entry['imdb_id'], self.list_id) self.session.post('http://www.imdb.com/list/_ajax/edit', data=dict(data, list_item_id=item_id), cookies=self.cookies) # We don't need to invalidate our cache if we remove the item self._items = [ i for i in self._items if i['imdb_id'] != entry['imdb_id'] ] if self._items else None def _add(self, entry): """Submit a new movie to imdb. (does not update cache)""" if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning( 'Cannot add %s to imdb_list because it does not have an imdb_id', entry['title']) return # Manually calling authenticate to fetch list_id and cookies self.authenticate() data = { 'const': entry['imdb_id'], 'list_id': self.list_id, 'ref_tag': 'title' } log.debug('adding title %s with ID %s to imdb %s', entry['title'], entry['imdb_id'], self.list_id) self.session.post('http://www.imdb.com/list/_ajax/edit', data=data, cookies=self.cookies) def add(self, entry): self._add(entry) # Invalidate the cache so that we get the canonical entry from the imdb list self.invalidate_cache() def __ior__(self, entries): for entry in entries: self._add(entry) self.invalidate_cache() return self def __len__(self): return len(self.items) @property def online(self): """ Set the online status of the plugin, online plugin should be treated differently in certain situations, like test mode""" return True def get(self, entry): return self._find_movie(entry)
from flexget import plugin, db_schema from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession from flexget.utils.soup import get_soup from flexget.utils.tools import parse_filesize log = logging.getLogger('filelist') Base = db_schema.versioned_base('filelist', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('filelist.ro', '2 seconds')) BASE_URL = 'https://filelist.ro/' CATEGORIES = { 'all': 0, 'anime': 24, 'audio': 11, 'cartoons': 15, 'docs': 16, 'games console': 10, 'games pc': 9, 'linux': 17, 'misc': 18, 'mobile': 22, 'movies 3d': 25,
from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import Session as RequestSession, TimedLimiter from flexget.utils.soup import get_soup from requests.exceptions import HTTPError, RequestException from datetime import datetime, date, timedelta import unicodedata import re log = logging.getLogger('search_npo') requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('npo.nl', '5 seconds')) class NPOWatchlist(object): """ Produces entries for every episode on the user's npo.nl watchlist (Dutch public television). Entries can be downloaded using http://arp242.net/code/download-npo If 'remove_accepted' is set to 'yes', the plugin will delete accepted entries from the watchlist after download is complete. If 'max_episode_age_days' is set (and not 0), entries will only be generated for episodes broadcast in the last x days. This only applies to episodes related to series the user is following. For example: npo_watchlist: email: [email protected]
from requests.exceptions import RequestException from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession from flexget.utils.requests import TimedLimiter plugin_name = 'pushsafer' log = logging.getLogger(plugin_name) PUSHSAFER_URL = 'https://www.pushsafer.com/api' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushsafer.com', '5 seconds')) class PushsaferNotifier: """ Example:: notify: entries: via: - pushsafer: private_key: <string> your private key (can also be a alias key) - Required url: <string> (default: '{{imdb_url}}') url_title: <string> (default: (none)) device: <string> ypur device or device group id (default: (none)) icon: <integer> (default is 1)
from __future__ import unicode_literals, division, absolute_import from builtins import * # noqa pylint: disable=unused-import, redefined-builtin import logging from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.cached_input import cached from flexget.utils.requests import RequestException, Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger("letterboxd") requests = Session(max_retries=5) requests.add_domain_limiter(TimedLimiter("letterboxd.com", "1 seconds")) base_url = "http://letterboxd.com" SLUGS = { "default": {"p_slug": "/%(user)s/list/%(list)s/", "f_slug": "data-film-slug"}, "diary": {"p_slug": "/%(user)s/films/diary/", "f_slug": "data-film-slug"}, "likes": {"p_slug": "/%(user)s/likes/films/", "f_slug": "data-film-link"}, "rated": {"p_slug": "/%(user)s/films/ratings/", "f_slug": "data-film-slug"}, "watched": {"p_slug": "/%(user)s/films/", "f_slug": "data-film-slug"}, "watchlist": {"p_slug": "/%(user)s/watchlist/", "f_slug": "data-film-slug"}, } SORT_BY = { "default": "", "added": "by/added/", "length-ascending": "by/shortest/",
from builtins import * # pylint: disable=unused-import, redefined-builtin import logging from flexget import plugin from flexget.event import event from flexget.plugin import PluginWarning from flexget.config_schema import one_or_more from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'join' log = logging.getLogger(plugin_name) requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('appspot.com', '5 seconds')) JOIN_URL = 'https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush' class JoinNotifier(object): """ Example:: notify: entries: via: - join: [api_key: <API_KEY> (your join api key. Only required for 'group' notifications)] [group: <GROUP_NAME> (name of group of join devices to notify. 'all', 'android', etc.) [device: <DEVICE_ID> (can also be a list of device ids)]
import logging from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'pushover' log = logging.getLogger(plugin_name) PUSHOVER_URL = 'https://api.pushover.net/1/messages.json' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushover.net', '5 seconds')) class PushoverNotifier(object): """ Example:: notify: entries: via: - pushover: user_key: <USER_KEY> (can also be a list of userkeys) token: <TOKEN> [device: <DEVICE_STRING>] [priority: <PRIORITY>] [url: <URL>]
from flexget import plugin, db_schema from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession from flexget.utils.soup import get_soup from flexget.config_schema import one_or_more from flexget.utils.tools import parse_filesize log = logging.getLogger('morethantv') Base = db_schema.versioned_base('morethantv', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter( 'morethan.tv', '5 seconds')) # TODO find out if they want a delay CATEGORIES = { 'Movies': 'filter_cat[1]', 'TV': 'filter_cat[2]', 'Other': 'filter_cat[3]' } TAGS = [ 'action', 'adventure', 'animation', 'anime', 'art', 'asian', 'biography', 'celebrities', 'comedy', 'cooking', 'crime', 'cult', 'documentary', 'drama', 'educational', 'elclasico', 'family', 'fantasy', 'film.noir', 'filmromanesc', 'food', 'football', 'formula.e', 'formula1', 'gameshow', 'highlights', 'history', 'horror', 'investigation', 'lifestyle', 'liga1', 'ligabbva', 'ligue1', 'martial.arts', 'morethan.tv', 'motogp', 'musical', 'mystery', 'nba', 'news', 'other', 'performance', 'philosophy', 'politics',
class InputWhatCD(object): """A plugin that searches what.cd == Usage: All parameters except `username` and `password` are optional. whatcd: username: password: user_agent: (A custom user-agent for the client to report. It is NOT A GOOD IDEA to spoof a browser with this. You are responsible for your account.) search: (general search filter) artist: (artist name) album: (album name) year: (album year) encoding: (encoding specifics - 192, 320, lossless, etc.) format: (MP3, FLAC, AAC, etc.) media: (CD, DVD, vinyl, Blu-ray, etc.) release_type: (album, soundtrack, EP, etc.) log: (log specification - true, false, '100%', or '<100%') hascue: (has a cue file - true or false) scene: (is a scene release - true or false) vanityhouse: (is a vanity house release - true or false) leech_type: ('freeleech', 'neutral', 'either', or 'normal') tags: (a list of tags to match - drum.and.bass, new.age, blues, etc.) tag_type: (match 'any' or 'all' of the items in `tags`) """ # Aliases for config -> api params ALIASES = { "artist": "artistname", "album": "groupname", "leech_type": "freetorrent", "release_type": "releaseType", "tags": "taglist", "tag_type": "tags_type", "search": "searchstr", "log": "haslog", } # API parameters # None means a raw value entry (no validation) # A dict means a choice with a mapping for the API # A list is just a choice with no mapping PARAMS = { "searchstr": None, "taglist": None, "artistname": None, "groupname": None, "year": None, "tags_type": { "any": 0, "all": 1, }, "encoding": [ "192", "APS (VBR)", "V2 (VBR)", "V1 (VBR)", "256", "APX (VBR)", "V0 (VBR)", "320", "lossless", "24bit lossless", "V8 (VBR)" ], "format": ["MP3", "FLAC", "AAC", "AC3", "DTS"], "media": [ "CD", "DVD", "vinyl", "soundboard", "SACD", "DAT", "cassette", "WEB", "Blu-ray" ], "releaseType": { "album": 1, "soundtrack": 3, "EP": 5, "anthology": 6, "compilation": 7, "DJ mix": 8, "single": 9, "live album": 11, "remix": 13, "bootleg": 14, "interview": 15, "mixtape": 16, "unknown": 21, "concert recording": 22, "demo": 23 }, "haslog": { "False": 0, "True": 1, "100%": 100, "<100%": -1 }, "freetorrent": { "freeleech": 1, "neutral": 2, "either": 3, "normal": 0, }, "hascue": { "False": 0, "True": 1, }, "scene": { "False": 0, "True": 1, }, "vanityhouse": { "False": 0, "True": 1, } } def _key(self, key): """Gets the API key name from the entered key""" if key in self.ALIASES: return self.ALIASES[key] return key def _opts(self, key): """Gets the options for the specified key""" return self.PARAMS[self._key(key)] def _getval(self, key, val): """Gets the value for the specified key based on a config option""" opts = self._opts(key) if isinstance(opts, dict): # Translate the input value to the What.CD API value # The str cast converts bools to 'True'/'False' for use as keys # This allows for options that have True/False/Other values return opts[str(val)] elif isinstance(val, list): # Fix yaml parser making a list out of a string return ",".join(val) return val def __init__(self): """Set up the schema""" self.schema = { 'type': 'object', 'properties': { 'username': { 'type': 'string' }, 'password': { 'type': 'string' }, 'user_agent': { 'type': 'string' }, 'search': { 'type': 'string' }, 'artist': { 'type': 'string' }, 'album': { 'type': 'string' }, 'year': { 'type': ['string', 'integer'] }, 'tags': one_or_more({'type': 'string'}), 'tag_type': { 'type': 'string', 'enum': list(self._opts('tag_type').keys()) }, 'encoding': { 'type': 'string', 'enum': self._opts('encoding') }, 'format': { 'type': 'string', 'enum': self._opts('format') }, 'media': { 'type': 'string', 'enum': self._opts('media') }, 'release_type': { 'type': 'string', 'enum': list(self._opts('release_type').keys()) }, 'log': { 'oneOf': [{ 'type': 'string', 'enum': list(self._opts('log').keys()) }, { 'type': 'boolean' }] }, 'leech_type': { 'type': 'string', 'enum': list(self._opts('leech_type').keys()) }, 'hascue': { 'type': 'boolean' }, 'scene': { 'type': 'boolean' }, 'vanityhouse': { 'type': 'boolean' }, }, 'required': ['username', 'password'], 'additionalProperties': False } def _login(self, user, passwd): """ Log in and store auth data from the server Adapted from https://github.com/isaaczafuta/whatapi """ data = { 'username': user, 'password': passwd, 'keeplogged': 1, } r = self.session.post("https://ssl.what.cd/login.php", data=data, allow_redirects=False) if r.status_code != 302 or r.headers.get('location') != "index.php": raise PluginError("Failed to log in to What.cd") accountinfo = self._request('index') self.authkey = accountinfo['authkey'] self.passkey = accountinfo['passkey'] log.info("Logged in to What.cd") def _request(self, action, page=None, **kwargs): """ Make an AJAX request to a given action page Adapted from https://github.com/isaaczafuta/whatapi """ ajaxpage = "https://ssl.what.cd/ajax.php" params = {} # Filter params and map config values -> api values for k, v in list(kwargs.items()): params[self._key(k)] = self._getval(k, v) # Params other than the searching ones params['action'] = action if page: params['page'] = page r = self.session.get(ajaxpage, params=params, allow_redirects=False) if r.status_code != 200: raise PluginError("What.cd returned a non-200 status code") try: json_response = r.json() if json_response['status'] != "success": # Try to deal with errors returned by the API error = json_response.get('error', json_response.get('status')) if not error or error == "failure": error = json_response.get('response', str(json_response)) raise PluginError("What.cd gave a failure response: " "'{}'".format(error)) return json_response['response'] except (ValueError, TypeError, KeyError) as e: raise PluginError("What.cd returned an invalid response") def _search_results(self, config): """Generator that yields search results""" page = 1 pages = None while True: if pages and page >= pages: break log.debug( "Attempting to get page {} of search results".format(page)) result = self._request('browse', page=page, **config) if not result['results']: break for x in result['results']: yield x pages = result.get('pages', pages) page += 1 def _get_entries(self, search_results): """Genertor that yields Entry objects from search results""" for result in search_results: # Get basic information on the release info = dict( (k, result[k]) for k in ('artist', 'groupName', 'groupYear')) # Releases can have multiple download options for tor in result['torrents']: temp = info.copy() temp.update( dict( (k, tor[k]) for k in ('media', 'encoding', 'format', 'torrentId'))) yield Entry( title="{artist} - {groupName} - {groupYear} " "({media} - {format} - {encoding})-{torrentId}.torrent". format(**temp), url="https://what.cd/torrents.php?action=download&" "id={}&authkey={}&torrent_pass={}".format( temp['torrentId'], self.authkey, self.passkey), torrent_seeds=tor['seeders'], torrent_leeches=tor['leechers'], # Size is returned in bytes, convert to MB for compat with the content_size plugin content_size=math.floor(tor['size'] / (1024**2))) @cached('whatcd') @plugin.internet(log) def on_task_input(self, task, config): """Search on What.cd""" self.session = Session() # From the API docs: "Refrain from making more than five (5) requests every ten (10) seconds" self.session.add_domain_limiter( TokenBucketLimiter('ssl.what.cd', 2, '2 seconds')) # Custom user agent user_agent = config.pop('user_agent', None) if user_agent: self.session.headers.update({"User-Agent": user_agent}) # Login self._login(config.pop('username'), config.pop('password')) # Logged in successfully, it's ok if nothing matches task.no_entries_ok = True # NOTE: Any values still in config at this point MUST be valid search parameters # Perform the search and parse the needed information out of the response results = self._search_results(config) return list(self._get_entries(results))
from dateutil.parser import parse as dateutil_parse from flexget import plugin, db_schema from flexget.config_schema import one_or_more from flexget.entry import Entry from flexget.event import event from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession, TimedLimiter, RequestException from flexget.utils.tools import parse_filesize log = logging.getLogger('passthepopcorn') Base = db_schema.versioned_base('passthepopcorn', 1) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('passthepopcorn.me', '5 seconds')) TAGS = [ 'action', 'adventure', 'animation', 'arthouse', 'asian', 'biography', 'camp', 'comedy', 'crime', 'cult', 'documentary', 'drama', 'experimental',
import logging from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.config_schema import one_or_more from flexget.utils.requests import Session, TimedLimiter, RequestException from flexget.components.sites.utils import normalize_scene from flexget.plugin import PluginError log = logging.getLogger('rarbg') requests = Session() requests.add_domain_limiter( TimedLimiter('torrentapi.org', '3 seconds') ) # they only allow 1 request per 2 seconds CATEGORIES = { 'all': 0, # Movies 'x264': 17, 'x264 720p': 45, 'x264 1080p': 44, 'x264 3D': 47, 'XviD': 14, 'XviD 720p': 48, 'Full BD': 42, # TV 'HDTV': 41, 'SDTV': 18,
import logging from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.cached_input import cached from flexget.utils.requests import RequestException, Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger('letterboxd') requests = Session(max_retries=5) requests.add_domain_limiter(TimedLimiter('letterboxd.com', '1 seconds')) base_url = 'http://letterboxd.com' SLUGS = { 'default': {'p_slug': '/%(user)s/list/%(list)s/', 'f_slug': 'data-film-slug'}, 'diary': {'p_slug': '/%(user)s/films/diary/', 'f_slug': 'data-film-slug'}, 'likes': {'p_slug': '/%(user)s/likes/films/', 'f_slug': 'data-film-link'}, 'rated': {'p_slug': '/%(user)s/films/ratings/', 'f_slug': 'data-film-slug'}, 'watched': {'p_slug': '/%(user)s/films/', 'f_slug': 'data-film-slug'}, 'watchlist': {'p_slug': '/%(user)s/watchlist/', 'f_slug': 'data-film-slug'}, } SORT_BY = { 'default': '', 'added': 'by/added/', 'length-ascending': 'by/shortest/', 'length-descending': 'by/longest/', 'name': 'by/name/', 'popularity': 'by/popular/',
from flexget.config_schema import one_or_more from flexget.entry import Entry from flexget.event import event from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import RequestException from flexget.utils.requests import Session as RequestSession from flexget.utils.requests import TimedLimiter from flexget.utils.soup import get_soup from flexget.utils.tools import parse_filesize logger = logger.bind(name='alpharatio') Base = db_schema.versioned_base('alpharatio', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('alpharatio.cc', '5 seconds')) # ElementZero confirmed with AlphaRato sysop 'jasonmaster' that they do want a 5 second limiter CATEGORIES = { 'tvsd': 'filter_cat[1]', 'tvhd': 'filter_cat[2]', 'tvuhd': 'filter_cat[3]', 'tvdvdrip': 'filter_cat[4]', 'tvpacksd': 'filter_cat[5]', 'tvpackhd': 'filter_cat[6]', 'tvpackuhd': 'filter_cat[7]', 'moviesd': 'filter_cat[8]', 'moviehd': 'filter_cat[9]', 'movieuhd': 'filter_cat[10]', 'moviepacksd': 'filter_cat[11]', 'moviepackhd': 'filter_cat[12]',
import datetime from flexget import plugin from flexget.event import event from flexget.config_schema import one_or_more from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = 'pushbullet' log = logging.getLogger(__name__) PUSHBULLET_URL = 'https://api.pushbullet.com/v2/pushes' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushbullet.com', '5 seconds')) class PushbulletNotifier(object): """ Example:: pushbullet: apikey: <API_KEY> [device: <DEVICE_IDEN> (can also be a list of device idens, or don't specify any idens to send to all devices)] [email: <EMAIL_ADDRESS> (can also be a list of user email addresses)] [channel: <CHANNEL_TAG> (you can only specify device / email or channel tag. cannot use both.)] [title: <MESSAGE_TITLE>] (default: "{{task}} - Download started" -- accepts Jinja2) [body: <MESSAGE_BODY>] (default: "{{series_name}} {{series_id}}" -- accepts Jinja2) Configuration parameters are also supported from entries (eg. through set).
import logging from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'pushsafer' log = logging.getLogger(plugin_name) PUSHSAFER_URL = 'https://www.pushsafer.com/api' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('pushsafer.com', '5 seconds')) class PushsaferNotifier(object): """ Example:: pushsafer: private_key: <string> your private key (can also be a alias key) - Required title: <string> (default: task name) body: <string> (default: '{{series_name}} {{series_id}}' ) url: <string> (default: '{{imdb_url}}') url_title: <string> (default: (none)) device: <string> ypur device or device group id (default: (none)) icon: <integer> (default is 1) sound: <integer> (default is (none))
from flexget import plugin, db_schema from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession from flexget.utils.soup import get_soup from flexget.config_schema import one_or_more from flexget.utils.tools import parse_filesize log = logging.getLogger('morethantv') Base = db_schema.versioned_base('morethantv', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('morethan.tv', '5 seconds')) # TODO find out if they want a delay CATEGORIES = { 'Movies': 'filter_cat[1]', 'TV': 'filter_cat[2]', 'Other': 'filter_cat[3]' } TAGS = [ 'action', 'adventure', 'animation', 'anime', 'art', 'asian', 'biography',
from builtins import * # noqa pylint: disable=unused-import, redefined-builtin import logging import re from flexget import plugin from flexget.event import event from flexget.plugins.internal.urlrewriting import UrlRewritingError from flexget.utils.requests import Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger("newpct") requests = Session() requests.headers.update({"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"}) requests.add_domain_limiter(TimedLimiter("imdb.com", "2 seconds")) class UrlRewriteNewPCT(object): """NewPCT urlrewriter.""" # urlrewriter API def url_rewritable(self, task, entry): url = entry["url"] rewritable_regex = "^http:\/\/(www.)?newpct1?.com\/.*" return re.match(rewritable_regex, url) and not url.endswith(".torrent") # urlrewriter API def url_rewrite(self, task, entry): entry["url"] = self.parse_download_page(entry["url"])
import xml.etree.ElementTree as ET from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException plugin_name = 'prowl' log = logging.getLogger(plugin_name) PROWL_URL = 'https://api.prowlapp.com/publicapi/add' requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('prowlapp.com', '5 seconds')) class ProwlNotifier(object): """ Send prowl notifications Example:: notify: entries: via: - prowl: api_key: xxxxxxx [application: application name, default FlexGet] [event: event title, default New Release]
from __future__ import unicode_literals, division, absolute_import from builtins import * # noqa pylint: disable=unused-import, redefined-builtin import logging from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.cached_input import cached from flexget.utils.requests import RequestException, Session, TimedLimiter from flexget.utils.soup import get_soup log = logging.getLogger('letterboxd') requests = Session(max_retries=5) requests.add_domain_limiter(TimedLimiter('letterboxd.com', '1 seconds')) base_url = 'http://letterboxd.com' SLUGS = { 'default': {'p_slug': '/%(user)s/list/%(list)s/', 'f_slug': 'data-film-slug'}, 'diary': {'p_slug': '/%(user)s/films/diary/', 'f_slug': 'data-film-slug'}, 'likes': {'p_slug': '/%(user)s/likes/films/', 'f_slug': 'data-film-link'}, 'rated': {'p_slug': '/%(user)s/films/ratings/', 'f_slug': 'data-film-slug'}, 'watched': {'p_slug': '/%(user)s/films/', 'f_slug': 'data-film-slug'}, 'watchlist': {'p_slug': '/%(user)s/watchlist/', 'f_slug': 'data-film-slug'}, } SORT_BY = { 'default': '', 'added': 'by/added/', 'length-ascending': 'by/shortest/',
class ImdbEntrySet(MutableSet): schema = { 'type': 'object', 'properties': { 'login': { 'type': 'string' }, 'password': { 'type': 'string' }, 'list': { 'type': 'string' }, 'force_language': { 'type': 'string', 'default': 'en-us' }, }, 'additionalProperties': False, 'required': ['login', 'password', 'list'], } def __init__(self, config): self.config = config self._session = RequestSession() self._session.add_domain_limiter(TimedLimiter('imdb.com', '5 seconds')) self._session.headers.update( {'Accept-Language': config.get('force_language', 'en-us')}) self.user_id = None self.list_id = None self.cookies = None self.hidden_value = None self._items = None self._authenticated = False @property def session(self): if not self._authenticated: self.authenticate() return self._session def get_user_id_and_hidden_value(self, cookies=None): try: if cookies: self._session.cookies = cookiejar_from_dict(cookies) # We need to allow for redirects here as it performs 1-2 redirects before reaching the real profile url response = self._session.get('https://www.imdb.com/profile', allow_redirects=True) except RequestException as e: raise PluginError(str(e)) user_id_match = re.search(r'ur\d+(?!\d)', response.url) if user_id_match: # extract the hidden form value that we need to do post requests later on try: soup = get_soup(response.text) self.hidden_value = soup.find('input', attrs={'id': '49e6c'})['value'] except Exception as e: log.warning( 'Unable to locate the hidden form value ' '49e6c' '. Without it, you might not be able to ' 'add or remove items. %s', e, ) return user_id_match.group() if user_id_match else None def authenticate(self): """Authenticates a session with IMDB, and grabs any IDs needed for getting/modifying list.""" cached_credentials = False with Session() as session: user = (session.query(IMDBListUser).filter( IMDBListUser.user_name == self.config.get( 'login')).one_or_none()) if user and user.cookies and user.user_id: log.debug('login credentials found in cache, testing') self.user_id = user.user_id if not self.get_user_id_and_hidden_value(cookies=user.cookies): log.debug('cache credentials expired') user.cookies = None self._session.cookies.clear() else: self.cookies = user.cookies cached_credentials = True if not cached_credentials: log.debug( 'user credentials not found in cache or outdated, fetching from IMDB' ) url_credentials = ( 'https://www.imdb.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.imdb.com%2Fap-signin-' 'handler&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&' 'openid.assoc_handle=imdb_mobile_us&openid.mode=checkid_setup&openid.claimed_id=http%3A%' '2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.ope' 'nid.net%2Fauth%2F2.0') try: # we need to get some cookies first self._session.get('https://www.imdb.com') r = self._session.get(url_credentials) except RequestException as e: raise PluginError(e.args[0]) soup = get_soup(r.content) form = soup.find('form', attrs={'name': 'signIn'}) inputs = form.select('input') data = dict((i['name'], i.get('value')) for i in inputs if i.get('name')) data['email'] = self.config['login'] data['password'] = self.config['password'] action = form.get('action') log.debug('email=%s, password=%s', data['email'], data['password']) self._session.headers.update({'Referer': url_credentials}) self._session.post(action, data=data) self._session.headers.update( {'Referer': 'https://www.imdb.com/'}) self.user_id = self.get_user_id_and_hidden_value() if not self.user_id: raise plugin.PluginError( 'Login to IMDB failed. Check your credentials.') self.cookies = self._session.cookies.get_dict( domain='.imdb.com') # Get list ID if user: for list in user.lists: if self.config['list'] == list.list_name: log.debug( 'found list ID %s matching list name %s in cache', list.list_id, list.list_name, ) self.list_id = list.list_id if not self.list_id: log.debug( 'could not find list ID in cache, fetching from IMDB') if self.config['list'] == 'watchlist': data = { 'consts[]': 'tt0133093', 'tracking_tag': 'watchlistRibbon' } wl_data = self._session.post( 'https://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies, ).json() try: self.list_id = wl_data['list_id'] except KeyError: raise PluginError( 'No list ID could be received. Please initialize list by ' 'manually adding an item to it and try again') elif self.config['list'] in IMMUTABLE_LISTS or self.config[ 'list'].startswith('ls'): self.list_id = self.config['list'] else: data = {'tconst': 'tt0133093'} list_data = self._session.post( 'https://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies, ).json() for li in list_data['items']: if li['wlb_text'] == self.config['list']: self.list_id = li['data_list_id'] break else: raise plugin.PluginError('Could not find list %s' % self.config['list']) user = IMDBListUser(self.config['login'], self.user_id, self.cookies) list = IMDBListList(self.list_id, self.config['list'], self.user_id) user.lists.append(list) session.merge(user) self._authenticated = True def invalidate_cache(self): self._items = None @property def items(self): if self._items is None: log.debug('fetching items from IMDB') try: r = self.session.get( 'https://www.imdb.com/list/export?list_id=%s&author_id=%s' % (self.list_id, self.user_id), cookies=self.cookies, ) lines = list(r.iter_lines(decode_unicode=True)) except RequestException as e: raise PluginError(e.args[0]) # Normalize headers to lowercase lines[0] = lines[0].lower() self._items = [] for row in csv.DictReader(lines): log.debug('parsing line from csv: %s', row) try: item_type = row['title type'].lower() name = row['title'] year = int(row['year']) if row['year'] != '????' else None created = (datetime.strptime(row['created'], '%Y-%m-%d') if row.get('created') else None) modified = (datetime.strptime(row['modified'], '%Y-%m-%d') if row.get('modified') else None) entry = Entry({ 'title': '%s (%s)' % (name, year) if year != '????' else name, 'url': row['url'], 'imdb_id': row['const'], 'imdb_url': row['url'], 'imdb_list_position': int(row['position']) if 'position' in row else None, 'imdb_list_created': created, 'imdb_list_modified': modified, 'imdb_list_description': row.get('description'), 'imdb_name': name, 'imdb_year': year, 'imdb_user_score': float(row['imdb rating']) if row['imdb rating'] else None, 'imdb_votes': int(row['num votes']) if row['num votes'] else None, 'imdb_genres': [genre.strip() for genre in row['genres'].split(',')], }) except ValueError as e: log.debug( 'no movie row detected, skipping. %s. Exception: %s', row, e) continue if item_type in MOVIE_TYPES: entry['movie_name'] = name entry['movie_year'] = year elif item_type in SERIES_TYPES: entry['series_name'] = name entry['series_year'] = year elif item_type in OTHER_TYPES: entry['title'] = name else: log.verbose( 'Unknown IMDB type entry received: %s. Skipping', item_type) continue self._items.append(entry) return self._items @property def immutable(self): if self.config['list'] in IMMUTABLE_LISTS: return '%s list is not modifiable' % self.config['list'] def _from_iterable(cls, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it) def __contains__(self, entry): return self.get(entry) is not None def __iter__(self): return iter(self.items) def discard(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning( 'Cannot remove %s from imdb_list because it does not have an imdb_id', entry['title'], ) return # Get the list item id item_ids = None urls = [] if self.config['list'] == 'watchlist': method = 'delete' data = { 'consts[]': entry['imdb_id'], 'tracking_tag': 'watchlistRibbon' } status = self.session.post( 'https://www.imdb.com/list/_ajax/watchlist_has', data=data, cookies=self.cookies).json() item_ids = status.get('has', {}).get(entry['imdb_id']) urls = ['https://www.imdb.com/watchlist/%s' % entry['imdb_id']] else: method = 'post' data = {'tconst': entry['imdb_id']} status = self.session.post( 'https://www.imdb.com/list/_ajax/wlb_dropdown', data=data, cookies=self.cookies).json() for a_list in status['items']: if a_list['data_list_id'] == self.list_id: item_ids = a_list['data_list_item_ids'] break for item_id in item_ids: urls.append('https://www.imdb.com/list/%s/li%s/delete' % (self.list_id, item_id)) if not item_ids: log.warning('%s is not in list %s, cannot be removed', entry['imdb_id'], self.list_id) return for url in urls: log.debug( 'found movie %s with ID %s in list %s, removing', entry['title'], entry['imdb_id'], self.list_id, ) self.session.request(method, url, data={'49e6c': self.hidden_value}, cookies=self.cookies) # We don't need to invalidate our cache if we remove the item self._items = ( [i for i in self._items if i['imdb_id'] != entry['imdb_id']] if self._items else None) def _add(self, entry): """Submit a new movie to imdb. (does not update cache)""" if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning( 'Cannot add %s to imdb_list because it does not have an imdb_id', entry['title']) return # Manually calling authenticate to fetch list_id and cookies and hidden form value self.authenticate() if self.config['list'] == 'watchlist': method = 'put' url = 'https://www.imdb.com/watchlist/%s' % entry['imdb_id'] else: method = 'post' url = 'https://www.imdb.com/list/%s/%s/add' % (self.list_id, entry['imdb_id']) log.debug('adding title %s with ID %s to imdb %s', entry['title'], entry['imdb_id'], self.list_id) self.session.request(method, url, cookies=self.cookies, data={'49e6c': self.hidden_value}) def add(self, entry): self._add(entry) # Invalidate the cache so that we get the canonical entry from the imdb list self.invalidate_cache() def __ior__(self, entries): for entry in entries: self._add(entry) self.invalidate_cache() return self def __len__(self): return len(self.items) @property def online(self): """ Set the online status of the plugin, online plugin should be treated differently in certain situations, like test mode""" return True def get(self, entry): if not entry.get('imdb_id'): log.debug( 'entry %s does not have imdb_id, cannot compare to imdb list items', entry) return None log.debug('finding %s in imdb list', entry['imdb_id']) for e in self.items: if e['imdb_id'] == entry['imdb_id']: return e log.debug('could not find %s in imdb list items', entry['imdb_id']) return None
class ImdbEntrySet(MutableSet): schema = { 'type': 'object', 'properties': { 'login': {'type': 'string'}, 'password': {'type': 'string'}, 'list': {'type': 'string'}, 'force_language': {'type': 'string', 'default': 'en-us'} }, 'additionalProperties': False, 'required': ['login', 'password', 'list'] } def __init__(self, config): self.config = config self._session = Session() self._session.add_domain_limiter(TimedLimiter('imdb.com', '5 seconds')) self._session.headers = {'Accept-Language': config.get('force_language', 'en-us')} self.user_id = None self.list_id = None self._items = None self._authenticated = False @property def session(self): if not self._authenticated: self.authenticate() return self._session def authenticate(self): """Authenticates a session with imdb, and grabs any IDs needed for getting/modifying list.""" try: r = self._session.get( 'https://www.imdb.com/ap/signin?openid.return_to=https%3A%2F%2Fwww.imdb.com%2Fap-signin-' 'handler&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&' 'openid.assoc_handle=imdb_mobile_us&openid.mode=checkid_setup&openid.claimed_id=http%3A%' '2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.ope' 'nid.net%2Fauth%2F2.0') except ConnectionError as e: raise PluginError(e.args[0]) soup = get_soup(r.content) inputs = soup.select('form#ap_signin_form input') data = dict((i['name'], i.get('value')) for i in inputs if i.get('name')) data['email'] = self.config['login'] data['password'] = self.config['password'] d = self._session.post('https://www.imdb.com/ap/signin', data=data) # Get user id by extracting from redirect url r = self._session.head('http://www.imdb.com/profile', allow_redirects=False) if not r.headers.get('location') or 'login' in r.headers['location']: raise plugin.PluginError('Login to imdb failed. Check your credentials.') self.user_id = re.search('ur\d+(?!\d)', r.headers['location']).group() # Get list ID if self.config['list'] == 'watchlist': data = {'consts[]': 'tt0133093', 'tracking_tag': 'watchlistRibbon'} wl_data = self._session.post('http://www.imdb.com/list/_ajax/watchlist_has', data=data).json() try: self.list_id = wl_data['list_id'] except KeyError: raise PluginError('No list ID could be received. Please initialize list by ' 'manually adding an item to it and try again') elif self.config['list'] in IMMUTABLE_LISTS or self.config['list'].startswith('ls'): self.list_id = self.config['list'] else: data = {'tconst': 'tt0133093'} list_data = self._session.post('http://www.imdb.com/list/_ajax/wlb_dropdown', data=data).json() for li in list_data['items']: if li['wlb_text'] == self.config['list']: self.list_id = li['data_list_id'] break else: raise plugin.PluginError('Could not find list %s' % self.config['list']) self._authenticated = True def invalidate_cache(self): self._items = None @property def items(self): if self._items is None: try: r = self.session.get('http://www.imdb.com/list/export?list_id=%s&author_id=%s' % (self.list_id, self.user_id)) except HTTPError as e: raise PluginError(e.args[0]) lines = r.iter_lines() # Throw away first line with headers next(lines) self._items = [] for row in csv.reader(lines): row = [unicode(cell, 'utf-8') for cell in row] log.debug('parsing line from csv: %s', ', '.join(row)) if not len(row) == 16: log.debug('no movie row detected, skipping. %s', ', '.join(row)) continue entry = Entry({ 'title': '%s (%s)' % (row[5], row[11]) if row[11] != '????' else '%s' % row[5], 'url': row[15], 'imdb_id': row[1], 'imdb_url': row[15], 'imdb_list_position': int(row[0]), 'imdb_list_created': datetime.strptime(row[2], '%a %b %d %H:%M:%S %Y') if row[2] else None, 'imdb_list_modified': datetime.strptime(row[3], '%a %b %d %H:%M:%S %Y') if row[3] else None, 'imdb_list_description': row[4], 'imdb_name': row[5], 'movie_name': row[5], 'imdb_year': int(row[11]) if row[11] != '????' else None, 'movie_year': int(row[11]) if row[11] != '????' else None, 'imdb_score': float(row[9]) if row[9] else None, 'imdb_user_score': float(row[8]) if row[8] else None, 'imdb_votes': int(row[13]) if row[13] else None, 'imdb_genres': [genre.strip() for genre in row[12].split(',')] }) self._items.append(entry) return self._items @property def immutable(self): if self.config['list'] in IMMUTABLE_LISTS: return '%s list is not modifiable' % self.config['list'] def _from_iterable(cls, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it) def __contains__(self, entry): if not entry.get('imdb_id'): log.debug('entry %s does not have imdb_id, skipping', entry) return False return any(e['imdb_id'] == entry['imdb_id'] for e in self.items) def __iter__(self): return iter(self.items) def discard(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot remove %s from imdb_list because it does not have an imdb_id', entry['title']) return # Get the list item id item_ids = None if self.config['list'] == 'watchlist': data = {'consts[]': entry['imdb_id'], 'tracking_tag': 'watchlistRibbon'} status = self.session.post('http://www.imdb.com/list/_ajax/watchlist_has', data=data).json() item_ids = status.get('has', {}).get(entry['imdb_id']) else: data = {'tconst': entry['imdb_id']} status = self.session.post('http://www.imdb.com/list/_ajax/wlb_dropdown', data=data).json() for a_list in status['items']: if a_list['data_list_id'] == self.list_id: item_ids = a_list['data_list_item_ids'] break if not item_ids: log.warning('%s is not in list %s, cannot be removed', entry['imdb_id'], self.list_id) return data = { 'action': 'delete', 'list_id': self.list_id, 'ref_tag': 'title' } for item_id in item_ids: self.session.post('http://www.imdb.com/list/_ajax/edit', data=dict(data, list_item_id=item_id)) # We don't need to invalidate our cache if we remove the item self._items = [i for i in self._items if i['imdb_id'] != entry['imdb_id']] if self._items else None def add(self, entry): if self.config['list'] in IMMUTABLE_LISTS: raise plugin.PluginError('%s lists are not modifiable' % ' and '.join(IMMUTABLE_LISTS)) if 'imdb_id' not in entry: log.warning('Cannot add %s to imdb_list because it does not have an imdb_id', entry['title']) return data = { 'const': entry['imdb_id'], 'list_id': self.list_id, 'ref_tag': 'title' } self.session.post('http://www.imdb.com/list/_ajax/edit', data=data) # Invalidate cache so that new movie info will be grabbed self.invalidate_cache() def __len__(self): return len(self.items) @property def online(self): """ Set the online status of the plugin, online plugin should be treated differently in certain situations, like test mode""" return True
from flexget import plugin, db_schema from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import TimedLimiter, RequestException from flexget.manager import Session from flexget.utils.database import json_synonym from flexget.utils.requests import Session as RequestSession from flexget.utils.soup import get_soup from flexget.utils.tools import parse_filesize log = logging.getLogger('filelist') Base = db_schema.versioned_base('filelist', 0) requests = RequestSession() requests.add_domain_limiter(TimedLimiter('filelist.ro', '2 seconds')) BASE_URL = 'https://filelist.ro/' CATEGORIES = { 'all': 0, 'anime': 24, 'audio': 11, 'cartoons': 15, 'docs': 16, 'games console': 10, 'games pc': 9, 'linux': 17, 'misc': 18, 'mobile': 22, 'movies 3d': 25,
from builtins import * # noqa pylint: disable=unused-import, redefined-builtin import logging from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.config_schema import one_or_more from flexget.utils.requests import Session, TimedLimiter, RequestException from flexget.utils.search import normalize_scene from flexget.plugin import PluginError log = logging.getLogger('rarbg') requests = Session() requests.add_domain_limiter(TimedLimiter('torrentapi.org', '3 seconds')) # they only allow 1 request per 2 seconds CATEGORIES = { 'all': 0, # Movies 'x264': 17, 'x264 720p': 45, 'x264 1080p': 44, 'x264 3D': 47, 'XviD': 14, 'XviD 720p': 48, 'Full BD': 42, # TV 'HDTV': 41,
from flexget import plugin from flexget.entry import Entry from flexget.event import event from flexget.utils.requests import Session as RequestSession, TimedLimiter from flexget.utils.soup import get_soup from requests.exceptions import HTTPError, RequestException from datetime import datetime, date, timedelta import unicodedata import re log = logging.getLogger('search_npo') requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter('npostart.nl', '8 seconds')) class NPOWatchlist(object): """ Produces entries for every episode on the user's npostart.nl watchlist (Dutch public television). Entries can be downloaded using http://arp242.net/code/download-npo If 'remove_accepted' is set to 'yes', the plugin will delete accepted entries from the watchlist after download is complete. If 'max_episode_age_days' is set (and not 0), entries will only be generated for episodes broadcast in the last x days. This only applies to episodes related to series the user is following. For example: npo_watchlist: email: [email protected]
import logging from flexget import plugin from flexget.event import event from flexget.config_schema import one_or_more from flexget.plugin import PluginWarning from flexget.utils.requests import Session as RequestSession, TimedLimiter from requests.exceptions import RequestException __name__ = "rapidpush" log = logging.getLogger(__name__) RAPIDPUSH_URL = "https://rapidpush.net/api" requests = RequestSession(max_retries=3) requests.add_domain_limiter(TimedLimiter("rapidpush.net", "5 seconds")) class RapidpushNotifier(object): """ Example:: rapidpush: apikey: xxxxxxx (can also be a list of api keys) [category: category, default FlexGet] [title: title, default New release] [group: device group, default no group] [message: the message, default {{title}}] [channel: the broadcast notification channel, if provided it will be send to the channel subscribers instead of your devices, default no channel] [priority: 0 - 6 (6 = highest), default 2 (normal)]