[
  {
    "path": ".gitignore",
    "content": "__pycache__"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"push\"]\n\tpath = push\n\turl = https://github.com/arcturus-script/push.git\n"
  },
  {
    "path": "bilibili.py",
    "content": "import requests as req\nfrom re import compile\nfrom tools import failed, handler, info, success\nfrom datetime import datetime\nimport time\n\n# 获取视频信息地址\nVIDEO_INFO = \"https://api.bilibili.com/x/web-interface/view\"\n\n# 获取用户信息\nPERSONAL_INFO = \"https://api.bilibili.com/x/space/myinfo\"\n\n# 直播签到\nLIVE_BROADCAST = \"https://api.live.bilibili.com/sign/doSign\"\n\n# 漫画签到\nCOMICS = \"https://manga.bilibili.com/twirp/activity.v1.Activity/ClockIn\"\n\n# 漫画签到信息\nCOMICS_INFO = \"https://manga.bilibili.com/twirp/activity.v1.Activity/GetClockInInfo\"\n\n# 获取热门推荐\nRECOMMAND = \"https://api.bilibili.com/x/web-interface/popular\"\n\n# 客户端分享视频\nVIDEO_SHARE = \"https://api.bilibili.com/x/web-interface/share/add\"\n\n# 投币\nCOIN = \"https://api.bilibili.com/x/web-interface/coin/add\"\n\n# 看视频\nVIDEO_CLICK = \"https://api.bilibili.com/x/click-interface/click/web/h5\"\n\nVIDEO_HEARTBEAT = \"https://api.bilibili.com/x/click-interface/web/heartbeat\"\n\n# 兑换硬币\nTO_COIN = \"https://api.live.bilibili.com/xlive/revenue/v1/wallet/silver2coin\"\n\n# 获取当日投币情况\nCOIN_LOG = \" https://api.bilibili.com/x/member/web/coin/log\"\n\nheaders = {\n        \"user-agent\": \"Mozilla/5.0\",\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"Referer\": \"https://www.bilibili.com/\",\n}\n\nclass BiliBili:\n    headers = headers.copy()\n\n    def __init__(self, **config) -> None:\n        self.cookie = config.get(\"cookie\")\n        self.options = config.get(\"options\", {})\n\n        self.sid = BiliBili.extract(\"sid\", self.cookie)\n        self.csrf = BiliBili.extract(\"bili_jct\", self.cookie)\n        self.uid = BiliBili.extract(\"DedeUserID\", self.cookie)\n        self.headers.update({\"Cookie\": self.cookie})\n\n    @staticmethod\n    def extract(key: str, cookie: str):\n        \"\"\"根据键从 cookie 中抽取数据\n\n        Args:\n            key: 需要抽取数据的键, 可能值 bili_jct, sid, DedeUserID\n            cookie (str): BiliBili 的 cookie\n        \"\"\"\n        regEx = compile(f\"(?<={key}=).+?(?=;)|(?<={key}=).+\")\n        csrf = regEx.findall(cookie)\n        if len(csrf) != 0:\n            return csrf[0]\n        else:\n            return \"\"\n\n    # 获取视频信息\n    @staticmethod\n    def get_video_info(bv):\n        try:\n            rep = req.get(\n                VIDEO_INFO,\n                params={\"bvid\": bv},\n                headers=BiliBili.headers,\n            ).json()\n\n            if rep[\"code\"] == 0:\n                data = rep[\"data\"]\n\n                return {\n                    \"bvid\": data[\"bvid\"],  # 视频 BV 号\n                    \"aid\": data[\"aid\"],  # 视频 AV 号\n                    \"duration\": data[\"duration\"],\n                    \"cid\": data[\"cid\"],\n                    \"title\": data[\"title\"],  # 视频标题\n                }\n            else:\n                failed(f\"获取视频信息失败, 原因: {rep['message']}\")\n        except Exception as ex:\n            failed(f\"获取视频信息时出错, 原因: {ex}\")\n\n    # 获取用户信息\n    def get_user_info(self):\n        try:\n            rep = req.get(PERSONAL_INFO, headers=self.headers).json()\n\n            if rep[\"code\"] == 0:\n                data = rep[\"data\"]\n\n                current_exp = data[\"level_exp\"][\"current_exp\"]\n                next_exp = data[\"level_exp\"][\"next_exp\"]\n\n                self.name = data[\"name\"]  # 用户名\n                self.level = data[\"level\"]  # 等级\n                self.coin = data[\"coins\"]  # 硬币数\n                self.exp = f\"{current_exp}/{next_exp}\"  # 经验\n                self.silence = data[\"silence\"]  # 不知道是什么\n                \n                success(f\"获取用户信息成功, 用户: {self.name}\")\n            else:\n                raise Exception(rep[\"message\"])\n        except Exception as ex:\n            failed(f\"获取用户信息时出错, 原因: {ex}\")\n\n            self.name = \"Unkown\"\n            self.level = \"lv0\"\n            self.coin = 0\n            self.exp = \"0/0\"\n            self.silence = \"Unkown\"\n\n    # 直播签到\n    def live_broadcast_checkin(self):\n        if not self.options.get(\"lb\", False):\n            return\n\n        try:\n            rep = req.get(LIVE_BROADCAST, headers=self.headers).json()\n\n            if rep[\"code\"] == 0:\n                # 签到成功\n                data = rep[\"data\"]\n\n                success(f\"直播签到: 奖励 {data['text']}\")\n\n                return {\n                    \"raward\": data[\"text\"],\n                    \"specialText\": data[\"specialText\"],\n                }\n            else:\n                raise Exception(rep[\"message\"])\n\n        except Exception as ex:\n            failed(f\"直播签到失败, {ex}\")\n\n    # 漫画签到\n    def comics_checkin(self):\n        if not self.options.get(\"comics\", False):\n            return\n\n        try:\n            rep = req.post(\n                COMICS,\n                headers=self.headers,\n                data={\n                    \"platform\": \"android\",\n                },\n            ).json()\n\n            if rep[\"code\"] == 0:\n                success(\"漫画签到完成\")\n\n                result = self.comics_checkin_info()\n\n                if result is not None:\n                    return result\n                else:\n                    return \"unkown\"\n            else:\n                raise Exception(rep.get(\"msg\", \"Unknown error\"))\n        except Exception as ex:\n            failed(f\"漫画签到失败, {ex}\")\n\n    def comics_checkin_info(self):\n        rep = req.post(COMICS_INFO, headers=self.headers).json()\n\n        if rep[\"code\"] == 0:\n            success(f\"获取漫画签到信息成功, 您已经连续签到 {rep['data']['day_count']} 天\")\n\n            return rep[\"data\"][\"day_count\"]\n        else:\n            failed(f\"获取漫画签到信息失败, 原因: {rep['msg']}\")\n\n    # 获取推荐视频\n    @staticmethod\n    def video_suggest(ps: int = 50, pn: int = 1) -> list or None:\n        \"\"\"\n        Args:\n            ps (int): 视频个数\n            pn (int): 第几页数据\n\n        Returns:\n            video_list: 一个列表, 例如\n            [\n                {\"aid\": 551162867, \"title\": \"2022我的世界拜年纪\", \"bvid\": xxx},\n                {\"aid\": 508722277, \"title\": \"B站UP主, 办了个电影节\", \"bvid\": yyy},\n                ...\n            ]\n        \"\"\"\n\n        rep = req.get(RECOMMAND, params={\"ps\": ps, \"pn\": pn},headers=headers).json()\n\n        if rep[\"code\"] == 0:\n            res = []\n\n            videos = rep[\"data\"][\"list\"]\n\n            for video in videos:\n                # 将视频主要信息保存到字典里\n                res.append(\n                    {\n                        \"aid\": video[\"aid\"],\n                        \"bvid\": video[\"bvid\"],\n                        \"title\": video[\"title\"],\n                    }\n                )\n\n            return res\n        else:\n            failed(f\"获取视频推荐列表失败, 原因: {rep['message']}\")\n\n            return [{\"bvid\": \"BV1LS4y1C7Pa\"}]\n\n    # 投币\n    def give_coin(self, videos, per_coin_num=1, select_like=0):\n        coined = self.getCoinLog()  # 已经投币数\n\n        max_coin = self.options.get(\"coins\", 0)\n\n        if max_coin == 0:\n            return\n\n        surplus = max_coin - coined\n        surplus = 0 if surplus < 0 else surplus\n\n        info(f\"还需投币 {surplus} 个\")\n        \n        coin_videos = []\n\n        for video in videos:\n            # 当已投币数超过想投币数时退出\n            if coined < max_coin:\n                data = {\n                    \"aid\": str(video[\"aid\"]),\n                    \"multiply\": per_coin_num,  # 每次投币多少个, 默认 1 个\n                    \"select_like\": select_like,  # 是否同时点赞, 默认不点赞\n                    \"cross_domain\": \"true\",\n                    \"csrf\": self.csrf,\n                }\n\n                rep = req.post(COIN, headers=self.headers, data=data).json()\n\n                if rep[\"code\"] == 0:\n                    # 投币成功\n                    success(f\"给[{video['title']}]投币成功\")\n\n                    coin_videos.append(video[\"title\"])\n\n                    coined += 1  # 投币次数加 1\n                else:\n                    # 投币失败\n                    failed(f\"给[{video['title']}]投币失败, 原因: {rep['message']}\")\n            else:\n                success(f\"投币完成, 今日共投了 {coined} 个硬币\")\n\n                break\n\n        return coin_videos\n\n    # 分享视频\n    def share_video(self, videos):\n        if not self.options.get(\"share\", False):\n            return\n\n        for video in videos:\n            # 分享视频\n            data = {\n                \"aid\": video[\"aid\"],\n                \"csrf\": self.csrf,\n            }\n\n            rep = req.post(VIDEO_SHARE, data=data, headers=self.headers).json()\n\n            if rep[\"code\"] == 0:\n                # 如果分享成功, 退出循环, 并返回分享的视频名\n                success(f\"分享视频完成, [{video['title']}]\")\n\n                return video[\"title\"]\n            else:\n                failed(f\"分享视频[{video['title']}]失败, {rep['message']}\")\n\n    # 每日看视频\n    def watch(self, bvid):\n        if not self.options.get(\"watch\", False):\n            return\n\n        video_info = BiliBili.get_video_info(bvid)\n\n        # 获取视频信息成功\n        if video_info:\n            data = {\n                \"aid\": video_info[\"aid\"],\n                \"cid\": video_info[\"cid\"],\n                \"part\": 1,\n                \"ftime\": int(time.time()),\n                \"jsonp\": \"jsonp\",\n                \"mid\": self.uid,\n                \"csrf\": self.csrf,\n                \"stime\": int(time.time()),\n            }\n\n            rep = req.post(VIDEO_CLICK, data=data, headers=self.headers).json()\n\n            # 进入视频页\n            if rep[\"code\"] == 0:\n                data = {\n                    \"aid\": video_info[\"aid\"],\n                    \"cid\": video_info[\"cid\"],\n                    \"jsonp\": \"jsonp\",\n                    \"mid\": self.uid,\n                    \"csrf\": self.csrf,\n                    \"played_time\": 0,\n                    \"pause\": False,\n                    \"play_type\": 1,\n                    \"realtime\": video_info[\"duration\"],\n                    \"start_ts\": int(time.time()),\n                }\n\n                rep = req.post(VIDEO_HEARTBEAT, data=data, headers=self.headers).json()\n\n                if rep[\"code\"] == 0:\n                    # 模拟观看视频\n                    time.sleep(5)\n\n                    data[\"played_time\"] = video_info[\"duration\"] - 1\n                    data[\"play_type\"] = 0\n                    data[\"start_ts\"] = int(time.time())\n\n                    rep = req.post(\n                        VIDEO_HEARTBEAT,\n                        data=data,\n                        headers=self.headers,\n                    ).json()\n\n                    if rep[\"code\"] == 0:\n                        success(f\"观看视频完成, [{video_info['title']}]\")\n\n                        return f\"观看视频[{video_info['title']}]成功\"\n\n            failed(f\"观看视频失败, [{video_info['title']}]\")\n\n    # 银瓜子兑换银币\n    def toCoin(self):\n        if not self.options.get(\"toCoin\", False):\n            return\n\n        resp = req.post(\n            TO_COIN,\n            headers=self.headers,\n            data={\n                \"csrf_token\": self.csrf,\n                \"csrf\": self.csrf,\n            },\n        ).json()\n\n        return resp.get(\"message\", \"兑换失败\")\n\n    def getCoinLog(self):\n        resp = req.get(\n            COIN_LOG,\n            headers=self.headers,\n            params={\n                \"csrf\": self.csrf,\n                \"jsonp\": \"jsonp\",\n            },\n        ).json()\n\n        res = 0\n\n        if resp.get(\"code\") == 0:\n            coin_log = resp.get(\"data\").get(\"list\")\n\n            today = datetime.today().date()\n            for i in coin_log:\n                t = datetime.strptime(i[\"time\"], \"%Y-%m-%d %H:%M:%S\")\n\n                if t.date() == today:\n                    if i[\"delta\"] < 0:\n                        res += -i[\"delta\"]\n            \n            success(f\"获取硬币投递情况成功, 当前已投币 {res} 个\")\n        else:\n            failed(\"获取投币情况失败\")\n\n        return res\n\n    @handler\n    def start(self):\n        self.get_user_info()  # 获取用户信息\n\n        videos = self.video_suggest()  # 获取热门视频\n\n        return {\n            \"name\": self.name,\n            \"level\": self.level,\n            \"coin\": self.coin,\n            \"exp\": self.exp,\n            \"coins\": self.give_coin(videos),  # 投币\n            \"share\": self.share_video(videos),  # 视频分享\n            \"comics\": self.comics_checkin(),  # 漫画签到\n            \"lb\": self.live_broadcast_checkin(),  # 直播签到\n            \"watch\": self.watch(videos[0][\"bvid\"]),  # 观看视频\n            \"toCoin\": self.toCoin(),  # 银瓜子兑换硬币,\n        }\n"
  },
  {
    "path": "config.py",
    "content": "config = {\n    \"multi\": [\n        {\n            \"cookie\": \"xxx\",\n            \"options\": {\n                \"watch\": True,  # 每日观看视频\n                \"coins\": 1,  # 投币个数\n                \"share\": True,  # 视频分享\n                \"comics\": True,  # 漫画签到\n                \"lb\": True,  # 直播签到\n                \"threshold\": 100,  # 仅剩多少币时不再投币(不写默认100)\n                \"toCoin\": False,  # 银瓜子兑换硬币\n            },\n            # \"push\": {\n            #     \"type\": \"pushplus\",\n            #     \"key\": \"xxx\",\n            # },\n        },\n        {\n            \"cookie\": \"xxx\",\n            \"options\": {\n                \"watch\": True,  # 每日观看视频\n                \"coins\": 2,  # 投币个数\n                \"share\": True,  # 视频分享\n                \"comics\": True,  # 漫画签到\n                \"lb\": True,  # 直播签到\n                \"toCoin\": False,  # 银瓜子兑换硬币\n            },\n            # \"push\": [\n            #     # 以数组的形式填写, 则会向多个服务推送消息\n            #     {\n            #         \"type\": \"pushplus\",\n            #         \"key\": \"xxx\",\n            #     },\n            #     {\n            #         \"type\": \"workWechat\",\n            #         \"key\": {\n            #             \"agentid\": 1000002,\n            #             \"corpSecret\": \"xxx\",\n            #             \"corpid\": \"xxx\",\n            #         },\n            #     },\n            # ],\n        },\n    ],\n    \"push\": {\n        # 只作用于在multi并未配置 push 的组\n        \"type\": \"pushplus\",\n        \"key\": \"xxx\",\n    },\n}\n"
  },
  {
    "path": "index.py",
    "content": "from bilibili import BiliBili\nfrom config import config\nfrom push import PushSender, parse\n\n\ndef parse_message(message, push_type):\n    if push_type == \"pushplus\":\n        return parse(message, template=\"html\")\n    else:\n        return parse(message, template=\"markdown\")\n\n\ndef pushMessage(message, config):\n    if isinstance(config, list):\n        for item in config:\n            t = item.get(\"type\")\n\n            p = PushSender(t, item.get(\"key\"))\n\n            p.send(parse_message(message, t), title=\"Bilibili\")\n    else:\n        t = config.get(\"type\")\n\n        p = PushSender(config.get(\"type\"), config.get(\"key\"))\n\n        p.send(parse_message(message, t), title=\"Bilibili\")\n\n\ndef main(*args):\n    accounts = config.get(\"multi\")\n    push_together = config.get(\"push\")\n\n    messages = []\n\n    for item in accounts:\n        obj = BiliBili(**item)\n\n        res = obj.start()\n\n        push = item.get(\"push\")\n\n        if push is None:\n            if push_together is not None:\n                messages.extend(res)\n        else:\n            pushMessage(res, push)\n\n    if len(messages) != 0 and push_together is not None:\n        pushMessage(messages, push_together)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "readme.md",
    "content": "## BiliBili(云函数版)\n\n### 实现功能\n\n- [x] 获取用户信息\n- [x] 直播签到\n- [x] 漫画签到\n- [x] 投币\n- [x] 分享视频\n- [x] 每日看视频\n- [x] 多账户支持\n- [x] 自动兑换银瓜子\n- [ ] 大会员积分签到(等我有大会员了再搞(╹ڡ╹ ))\n\n### 步骤\n\n注意把子模块也一起下载\n\n```bash\ngit clone --recursive https://github.com/arcturus-script/bilibili.git\n```\n\n直接配置 config.py 即可, 入口改为 index.main\n"
  },
  {
    "path": "tools.py",
    "content": "def handler(fn):\n    def inner(*args, **kwargs):\n        res = fn(*args, **kwargs)\n\n        content = [\n            {\n                \"h4\": {\n                    \"content\": res[\"name\"],\n                },\n            },\n            {\n                \"txt\": {\n                    \"content\": f\"等级: {res['level']}\",\n                },\n            },\n            {\n                \"txt\": {\n                    \"content\": f\"硬币: {res['coin']}\",\n                },\n            },\n            {\n                \"txt\": {\n                    \"content\": f\"经验: {res['exp']}\",\n                },\n            },\n        ]\n\n        watch = res.get(\"watch\")\n\n        if watch is not None:\n            content.append(\n                {\n                    \"txt\": {\n                        \"content\": watch,\n                    }\n                }\n            )\n\n        share = res.get(\"share\")\n\n        if share is not None:\n            content.append(\n                {\n                    \"txt\": {\n                        \"content\": f\"分享视频: {share}\",\n                    }\n                }\n            )\n\n        coins = res.get(\"coins\")\n\n        if coins is not None:\n            content.append(\n                {\n                    \"h5\": {\n                        \"content\": \"投币\",\n                    },\n                    \"orderedList\": {\n                        \"content\": coins,\n                    },\n                }\n            )\n\n        comics = res.get(\"comics\")\n\n        if comics is not None:\n            content.extend(\n                [\n                    {\n                        \"h5\": {\n                            \"content\": \"漫画签到\",\n                        },\n                        \"txt\": {\n                            \"content\": f\"连续签到 {comics} 天\",\n                        },\n                    },\n                ]\n            )\n\n        lb = res.get(\"lb\")\n\n        if lb is not None:\n            content.extend(\n                [\n                    {\n                        \"h5\": {\n                            \"content\": \"直播\",\n                        },\n                        \"txt\": {\n                            \"content\": lb[\"raward\"],\n                        },\n                    },\n                ]\n            )\n\n        toCoin = res.get(\"toCoin\")\n\n        if toCoin is not None:\n            content.append(\n                {\n                    \"h5\": {\n                        \"content\": \"银瓜子兑换硬币\",\n                    },\n                    \"txt\": {\n                        \"content\": toCoin,\n                    },\n                }\n            )\n\n        return content\n\n    return inner\n\n\ndef failed(*args, **kwargs):\n    print(\"[\\033[31mfailed\\033[0m]  \", end=\"\")\n    print(*args, **kwargs)\n\n\ndef success(*args, **kwargs):\n    print(\"[\\033[32msuccess\\033[0m] \", end=\"\")\n    print(*args, **kwargs)\n\n\ndef info(*args, **kwargs):\n    print(\"[\\033[34minfo\\033[0m]    \", end=\"\")\n    print(*args, **kwargs)"
  }
]