马春杰杰 Exit Reader Mode

使用flexget过滤免费种

参考GitHub大神的项目,不过目前大佬好像不更新了,我只用它rss皇后的免费种,不过貌似有点问题,还好有另一位大佬解决了,提了PR,我在这里备份一下,防止以后自己忘记。如果有大佬还不会用flexget,可以先参考:

首先在flexget目录下新建plugins目录:

~/.flexget/plugins/  # Linux
C:\Users\<YOURUSER>\flexget\plugins\  # Windows

然后,新建nexusphp.py文件,填入以下内容:

# coding=utf-8
from __future__ import unicode_literals, division, absolute_import

import time
from builtins import *

import concurrent.futures
import re
import logging
from datetime import datetime

from requests.adapters import HTTPAdapter

from flexget import plugin
from flexget.config_schema import one_or_more
from flexget.event import event
from flexget.utils.soup import get_soup
from flexget.utils.tools import parse_timedelta

log = logging.getLogger('nexusphp')


class NexusPHP(object):
    """
    配置示例
    task_name:
        rss:
            url: https://www.example.com/rss.xml
            other_fields:
                - link
        nexusphp:
            cookie: 'my_cookie'
            discount:
                - free
                - 2x
            seeders:
                min: 1
                max: 30
            leechers:
                min: 1
                max: 100
                max_complete: 0.8
            hr: no
    """

    schema = {
        'type': 'object',
        'properties': {
            'cookie': {'type': 'string'},
            'discount': one_or_more({'type': 'string', 'enum': ['free', '2x', '2xfree', '30%', '50%', '2x50%']}),
            'seeders': {
                'type': 'object',
                'properties': {
                    'min': {'type': 'integer', 'minimum': 0, 'default': 0},
                    'max': {'type': 'integer', 'minimum': 0, 'default': 100000}
                }
            },
            'leechers': {
                'type': 'object',
                'properties': {
                    'min': {'type': 'integer', 'minimum': 0, 'default': 0},
                    'max': {'type': 'integer', 'minimum': 0, 'default': 100000},
                    'max_complete': {'type': 'number', 'minimum': 0, 'maximum': 1, 'default': 1}
                }
            },
            'left-time': {'type': 'string', 'format': 'interval'},
            'hr': {'type': 'boolean'},
            'adapter': {
                'type': 'object',
                'properties': {
                    'free': {'type': 'string', 'default': 'free'},
                    '2x': {'type': 'string', 'default': 'twoup'},
                    '2xfree': {'type': 'string', 'default': 'twoupfree'},
                    '30%': {'type': 'string', 'default': 'thirtypercent'},
                    '50%': {'type': 'string', 'default': 'halfdown'},
                    '2x50%': {'type': 'string', 'default': 'twouphalfdown'}
                }
            },
            'comment': {'type': 'boolean'},
            'user-agent': {'type': 'string'},
            'remember': {'type': 'boolean', 'default': True}
        },
        'required': ['cookie']
    }

    @staticmethod
    def build_config(config):
        config = dict(config)
        config.setdefault('discount', None)
        config.setdefault('seeders', None)
        config.setdefault('leechers', None)
        config.setdefault('left-time', None)
        config.setdefault('hr', True)
        config.setdefault('adapter', None)
        config.setdefault('user-agent',
                          'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko)'
                          'Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45')
        return config

    @plugin.priority(127)
    def on_task_modify(self, task, config):
        if config.get('comment', False):
            for entry in task.entries:
                if 'torrent' in entry and 'link' in entry:
                    entry['torrent'].content['comment'] = entry['link']
                    entry['torrent'].modified = True

    def on_task_filter(self, task, config):
        config = self.build_config(config)

        adapter = HTTPAdapter(max_retries=5)
        task.requests.mount('http://', adapter)
        task.requests.mount('https://', adapter)

        # 先访问一次 预防异常
        headers = {
            'cookie': config['cookie'],
            'user-agent': config['user-agent']
        }
        try:
            task.requests.get(task.entries[0].get('link'), headers=headers)
        except Exception:
            pass

        def consider_entry(_entry, _link):
            try:
                discount, seeders, leechers, hr, expired_time = NexusPHP._get_info(task, _link, config)
            except plugin.PluginError as e:
                raise e
            except Exception as e:
                log.info('NexusPHP._get_info: ' + str(e))
                return

            remember = config['remember']

            if config['discount']:
                if discount not in config['discount']:
                    _entry.reject('%s does not match discount' % discount, remember=remember)  # 优惠信息不匹配
                    return

            if config['left-time'] and expired_time:
                left_time = expired_time - datetime.now()
                # 实际剩余时间 < 'left-time'
                if left_time < parse_timedelta(config['left-time']):
                    _entry.reject('its discount time only left [%s]' % left_time, remember=remember)  # 剩余时间不足
                    return

            if config['hr'] is False and hr:
                _entry.reject('it is HR', remember=True)  # 拒绝HR

            if config['seeders']:
                seeder_max = config['seeders']['max']
                seeder_min = config['seeders']['min']
                if len(seeders) not in range(seeder_min, seeder_max + 1):
                    _entry.reject('%d is out of range of seeder' % len(seeders), remember=True)  # 做种人数不匹配
                    return

            if config['leechers']:
                leecher_max = config['leechers']['max']
                leecher_min = config['leechers']['min']
                if len(leechers) not in range(leecher_min, leecher_max + 1):
                    _entry.reject('%d is out of range of leecher' % len(leechers), remember=True)  # 下载人数不匹配
                    return

                if len(leechers) != 0:
                    max_complete = max(leechers, key=lambda x: x['completed'])['completed']
                else:
                    max_complete = 0
                if max_complete > config['leechers']['max_complete']:
                    _entry.reject('%f is more than max_complete' % max_complete, remember=True)  # 最大完成度不匹配
                    return

            _entry.accept()

        futures = []  # 线程任务
        with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
            for entry in task.accepted + task.undecided:
                link = entry.get('link')
                if not link:
                    raise plugin.PluginError("The rss plugin require 'other_fields' which contain 'link'. "
                                             "For example: other_fields: - link")
                futures.append(executor.submit(consider_entry, entry, link))
                time.sleep(0.5)

        for f in concurrent.futures.as_completed(futures):
            exception = f.exception()
            if isinstance(exception, plugin.PluginError):
                log.error(exception)

    @staticmethod
    # 解析页面,获取优惠、做种者信息、下载者信息
    def info_from_page(detail_page, peer_page, discount_fn, hr_fn=None):
        try:
            discount, expired_time = discount_fn(detail_page)
        except Exception:
            discount = expired_time = None  # 无优惠

        try:
            if hr_fn:
                hr = hr_fn(detail_page)
            else:
                hr = False
                for item in ['hitandrun', 'hit_run.gif', 'Hit and Run', 'Hit & Run']:
                    if item in detail_page.text:
                        hr = True
                        break
        except Exception:
            hr = False  # 无HR

        soup = get_soup(peer_page.replace('\n', ''), 'html5lib')
        seeders = leechers = []
        tables = soup.find_all('table', limit=2)
        if len(tables) == 2:                                     # 1. seeder leecher 均有
            seeders = NexusPHP.get_peers(tables[0])
            leechers = NexusPHP.get_peers(tables[1])
        elif len(tables) == 1 and len(soup.body.contents) == 3:  # 2. seeder leecher 有其一
            nodes = soup.body.contents
            if nodes[1].name == 'table':                    # 2.1 只有seeder 在第二个节点
                seeders = NexusPHP.get_peers(nodes[1])
            else:                                           # 2.2 只有leecher 在第三个节点
                leechers = NexusPHP.get_peers(nodes[2])
        else:                                                    # 3. seeder leecher 均无
            seeders = leechers = []
        return discount, seeders, leechers, hr, expired_time

    @staticmethod
    def get_peers(table):
        peers = []
        name_index = 0
        connectable_index = 1
        uploaded_index = 2
        downloaded_index = 4
        completed_index = 7
        for index, tr in enumerate(table.find_all('tr')):
            try:
                if index == 0:
                    tds = tr.find_all('td')
                    for i, td in enumerate(tds):
                        text = td.get_text()
                        if text in ['用户', '用戶', '会员/IP']:
                            name_index = i
                        elif text in ['可连接', '可連接', '公网']:
                            connectable_index = i
                        elif text in ['上传', '上傳', '总上传']:
                            uploaded_index = i
                        elif text in ['下载', '下載', '本次下载']:
                            downloaded_index = i
                        elif text == '完成':
                            completed_index = i
                else:
                    tds = tr.find_all('td')
                    peers.append({
                        'name': tds[name_index].get_text(),
                        'connectable': True if tds[connectable_index].get_text() != '是' else False,
                        'uploaded': tds[uploaded_index].get_text(),
                        'downloaded': tds[downloaded_index].get_text(),
                        'completed': float(tds[completed_index].get_text().strip('%')) / 100
                    })
            except Exception:
                peers.append({
                    'name': '',
                    'connectable': False,
                    'uploaded': '',
                    'downloaded': '',
                    'completed': 0
                })
        return peers

    @staticmethod
    def _get_info(task, link, config):
        headers = {
            'cookie': config['cookie'],
            'user-agent': config['user-agent']
        }
        if 'open.cd' in link:
            link = link.replace('details.php', 'plugin_details.php')
        detail_page = task.requests.get(link, headers=headers, timeout=20)  # 详情
        detail_page.encoding = 'utf-8'

        def get_peer_page():
            if 'totheglory' in link:
                return ''
            peer_url = link.replace('details.php', 'viewpeerlist.php', 1)
            try:
                if config['seeders'] or config['leechers']:  # 配置了seeders、leechers才请求
                    return task.requests.get(peer_url, headers=headers).text  # peer详情
            except Exception:
                return ''
            return ''

        peer_page = get_peer_page()

        if 'login' in detail_page.url or 'portal.php' in detail_page.url:
            raise plugin.PluginError("Can't access the site. Your cookie may be wrong!")

        if config['adapter']:
            convert = {value: key for key, value in config['adapter'].items()}
            discount_fn = NexusPHP.generate_discount_fn(convert)
            return NexusPHP.info_from_page(detail_page, peer_page, discount_fn)

        sites_discount = {
            'chdbits': {
                'pro_free.*?</h1>': 'free',
                'pro_2up.*?</h1>': '2x',
                'pro_free2up.*?</h1>': '2xfree',
                'pro_30pctdown.*?</h1>': '30%',
                'pro_50pctdown.*?</h1>': '50%',
                'pro_50pctdown2up.*?</h1>': '2x50%'
            },
            'u2.dmhy': {
                'class=.pro_free.*?promotion.*?</td>': 'free',
                'class=.pro_2up.*?promotion.*?</td>': '2x',
                'class=.pro_free2up.*?promotion.*?</td>': '2xfree',
                'class=.pro_30pctdown.*?promotion.*?</td>': '30%',
                'class=.pro_50pctdown.*?promotion.*?</td>': '50%',
                'class=.pro_50pctdown2up.*?promotion.*?</td>': '2x50%',
                'class=.pro_custom.*?0\.00X.*?promotion.*?</td>': '2xfree'
            },
            'totheglory': {
                '本种子限时不计流量.*?</font>': 'free',
                '本种子的下载流量计为实际流量的30%.*?</font>': '30%',
                '本种子的下载流量会减半.*?</font>': '50%',
            },
            'hdchina': {
                'pro_free.*?</h2>': 'free',
                'pro_2up.*?</h2>': '2x',
                'pro_free2up.*?</h2>': '2xfree',
                'pro_30pctdown.*?</h2>': '30%',
                'pro_50pctdown.*?</h2>': '50%',
                'pro_50pctdown2up.*?</h2>': '2x50%'
            }
        }
        for site, convert in sites_discount.items():
            if site in link:
                discount_fn = NexusPHP.generate_discount_fn(convert)
                return NexusPHP.info_from_page(detail_page, peer_page, discount_fn)
        discount_fn = NexusPHP.generate_discount_fn({
            'class=\'free\'.*?免.*?</h1>': 'free',
            'class=\'twoup\'.*?2X.*?</h1>': '2x',
            'class=\'twoupfree\'.*?2X免.*?</h1>': '2xfree',
            'class=\'thirtypercent\'.*?30%.*?</h1>': '30%',
            'class=\'halfdown\'.*?50%.*?</h1>': '50%',
            'class=\'twouphalfdown\'.*?2X 50%.*?</h1>': '2x50%'
        })
        return NexusPHP.info_from_page(detail_page, peer_page, discount_fn)

    @staticmethod
    def generate_discount_fn(convert):
        def fn(page):
            html = page.text.replace('\n', '')
            for key, value in convert.items():
                match = re.search(key, html)
                if match:
                    discount_str = match.group(0)
                    expired_time = None
                    # 匹配优惠剩余时间
                    match = re.search('(\d{4})(-\d{1,2}){2}\s\d{1,2}(:\d{1,2}){2}', discount_str)
                    if match:
                        expired_time_str = match.group(0)
                        expired_time = datetime.strptime(expired_time_str, "%Y-%m-%d %H:%M:%S")
                    return value, expired_time
            return None, None

        return fn


@event('plugin.register')
def register_plugin():
    plugin.register(NexusPHP, 'nexusphp', api_ver=2)

注意更换user-agent为自己的。

然后打开flexget配置文件,修改为以下:

tasks:
#  hdsky:
#    rss: https://hdsky
#    accept_all: no
#    content_size: #启用大小过滤
#      min: 10240 # 文件小于 2048M 就不下载
#      max: 999900 # 文件大于 9999M 就不下载
#      strict: no #不要动
#    download: /Users/mcj/Downloads/CCC不备份/flexget/hdsky
  opencd:
    rss:
      url: https://open
      other_fields:
        - link
    accept_all: yes
    download: /Users/mcj/Downloads/CCC不备份/flexget/opencd
    nexusphp:
      cookie: '_ga=GAxxxxxxxxxxxx208746'
      discount:
        - free
        - 2x
      adapter:
        2x50%: pro_50pctdown2up
        2x: pro_2up
        2xfree: pro_free2up
        30%: pro_30pctdown
        50%: pro_50pctdown
        free: pro_free

接下来就OK了。

想使用flexget过滤指定大小的种子?

本文最后更新于2021年4月15日,已超过 1 年没有更新,如果文章内容或图片资源失效,请留言反馈,我们会及时处理,谢谢!