├── .gitignore ├── LICENSE ├── config.ini ├── helper ├── get_categories.py ├── get_category_from_video.py └── subscribe_channel.py ├── readme.md ├── requirements.txt └── src ├── dl_schedule.py ├── manually_dl.py ├── modules ├── config.py ├── database.py └── video.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | .idea/* 133 | 134 | # VScode 135 | .vscode/* 136 | 137 | *.bat 138 | 139 | thumbnail/ 140 | videos/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [cookie] 2 | sessdata = 看readme 3 | bili_jct = 看readme 4 | 5 | [database] 6 | host = 数据库ip或者(域名也可?) 7 | user = 数据库连接用户名 8 | password = 数据库连接密码 9 | name = 数据库名称 10 | 11 | [time] 12 | setup_time = 2020-12-22T20:36:30 #这个以后会加的(画饼.jpg) 13 | 14 | [bilibili] 15 | #前三个都是给没在数据库里找到频道id的时候才用 16 | tid = 自己用helper里的脚本获取分区id 17 | tag = 标签1,多标签用英文逗号隔开,标签3 18 | desc_len = 简介长度,不同分区长度不同 19 | #True则把视频信息(频道名,日期放简介里) 20 | video_info = True 21 | 22 | [server] 23 | callback_server = https://你服务器的域名或ip/feed/ 比如 https://yomama.cc/feed/ 或者 https://123.45.67.89/feed/ 24 | -------------------------------------------------------------------------------- /helper/get_categories.py: -------------------------------------------------------------------------------- 1 | from bilibili_api import channel 2 | 3 | print("输入分区id") 4 | tid = input() 5 | print(channel.get_channel_info_by_tid(int(tid))) 6 | -------------------------------------------------------------------------------- /helper/get_category_from_video.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from bilibili_api import video 3 | from bilibili_api import aid2bvid 4 | 5 | async def main(): 6 | # 实例化 Video 类 7 | print("输入bv号") 8 | video_id = input() 9 | 10 | v = video.Video(bvid=video_id) 11 | # 获取信息 12 | info = await v.get_info() 13 | # 打印信息 14 | print(f"分区ID: {info['tid']} - 分区名字: {info['tname']}") 15 | 16 | if __name__ == '__main__': 17 | asyncio.get_event_loop().run_until_complete(main()) -------------------------------------------------------------------------------- /helper/subscribe_channel.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import requests 3 | import configparser 4 | 5 | 6 | parser = argparse.ArgumentParser() 7 | 8 | parser.add_argument("-i", "--id", help="channel id to subscribe") 9 | parser.add_argument("-u", "--unsubscribe", help="unsubscribe this channel", action="store_true") 10 | parser.add_argument("-ls", "--lease_seconds", help="time of subscribing a channel in second, 5 days if empty", action="count") 11 | 12 | args = parser.parse_args() 13 | 14 | if args.id is None: 15 | print("[!] channel id cannot be empty.") 16 | exit() 17 | 18 | mode = "subscribe" 19 | if args.unsubscribe: 20 | mode = "unsubscribe" 21 | 22 | if args.lease_seconds is None: 23 | args.lease_seconds = 60*60*24*5 24 | 25 | def setup_subscription(callback_server, channel_id, mode, lease_seconds: int): 26 | r = requests.post('https://pubsubhubbub.appspot.com/subscribe', data={ 27 | 'hub.mode': mode, 28 | 'hub.topic': 'https://www.youtube.com/xml/feeds/videos.xml?channel_id={}'.format(channel_id), 29 | 'hub.callback': callback_server, 30 | "hub.lease_seconds": lease_seconds, 31 | "hub.verify": 'async', 32 | }, headers={"content-type": "application/x-www-form-urlencoded"}) 33 | return r.status_code 34 | 35 | config = configparser.RawConfigParser() 36 | config.read("config.ini", encoding="utf-8") 37 | callback_server = config.get("server", "callback_server") 38 | 39 | code = setup_subscription(callback_server, args.id, mode, args.lease_seconds) 40 | 41 | if code in range(200, 204): 42 | print(f"[+] Successfully {mode}d the channel") -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # YoutubeToBilibili 2 | 一个自动搬运youtube视频到哔哩哔哩的python脚本 3 | 4 | # 声明 5 | 如果遇到不能用的情况请自行排查, 请勿跟图下这个人一样到别的repository开无关issue. 目前脚本可以正常运行(2022-04-26 12:32 UTC+8) 6 | 7 | ![不要当这个人](https://i.imgur.com/uNhrUPC.png) 8 | ![不要当这个人](https://i.imgur.com/TQK6FHF.png) 9 | 10 | # 使用前注意事项 11 | 12 | **请在 config.ini 的所在目录下运行py文件** 13 | 14 | 1. 使用 ``pip install -r requirements.txt --upgrade`` 安装依赖项 15 | 16 | 2. 更改 ``config.ini`` 里的配置文件 17 | - ``sessdata`` 与 ``bili_jct`` 的获取方法[看这里](https://github.com/Passkou/bilibili_api#获取-sessdate-和csrf) 18 | - 如果你想通过数据库来管理视频请看下面 19 | - 数据库表结构可以参考 ``database.py`` 进行创建 或 在连接到数据库服务器后,输入以下指令进行创建 20 | ``` 21 | create database youtube_video character set utf8mb4 collate utf8mb4_unicode_ci; 22 | 23 | use youtube_video; 24 | 25 | create table channel (channel_id varchar(255), subscribe_mode varchar(255), type int, primary key(channel_id)); 26 | 27 | create table task (id varchar(255), status int, primary key(id)); 28 | 29 | create table channel_type (row_ int not null auto_increment, tag varchar(255), category_id int not null, description_length int not null, primary key(row_)); 30 | 31 | 如果已经有channel这个表则输入这个指令: 32 | alter table channel add type int; 33 | ``` 34 | - 更改 ``tid`` 以指定上传视频的分区 35 | - 更改 ``tag`` 以指定上传视频的标签 36 | - 更改 ``desc_len`` 以指定简介最大长度(这个怪b站,因为每个分区的简介长度都不一样) 37 | - 同时在数据库里给 ``channel_type`` 插入数据,以指定上传时的信息(如果有则优先用数据库里的) 38 | - 如果需要使用订阅服务器的话,更改 ``callback_server`` 的值 39 | - 如果你只想手动用脚本完成下载上传就用 [src/manually_dl.py](https://github.com/HorizonKinen/YoutubeToBilibili/blob/master/src/manually_dl.py),如果需要数据库管理则加 ``--db 或 -d (比如python3 src/manually_dl.py -d)`` 40 | 41 | 3. 确保有安装 [ffmpeg](https://ffmpeg.org/download.html) 42 | - Windows 用户可能即使安装了也会出错,这时候需要把相应的 ``.exe`` 文件放在 ``python安装路径/Scripts`` 目录下 43 | - Linux 用户可通过自己的 package manager 进行安装 44 | 45 | 4. 确保 ``config.ini`` 中的 ``cookie`` 是最新的 46 | 47 | # 关于订阅服务器 48 | 由于我自己是本地使用所以我用 [ngrok](https://ngrok.com/) 把内网暴露给外网,你也可以把订阅服务器架设在服务器上之类的(反正只是拿一个订阅信息,国内服务器应该不会被gfw拦截吧..) 49 | 50 | 51 | # 感谢 52 | 53 | * [Freak](https://github.com/Fre-ak) - 为我写该脚本时所提供的帮助 54 | * Revolution - 在数据结构上的指点 55 | * [MoyuScript](https://github.com/MoyuScript) - 集成b站的api 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bilibili_api>=9.1.0 2 | pillow 3 | youtube-dl 4 | mysql-connector 5 | sqlalchemy 6 | flask 7 | flask-sqlalchemy 8 | feedparser 9 | schedule -------------------------------------------------------------------------------- /src/dl_schedule.py: -------------------------------------------------------------------------------- 1 | import schedule 2 | import time 3 | from modules.database import task 4 | from modules.video import download 5 | 6 | def download_from_tasks(): 7 | new = task.get_task_status(1) 8 | count = 0 9 | for t in new: 10 | url = "https://www.youtube.com/watch?v={}".format(t.get_video_id()) 11 | print("[-] downloading {} | status: {}".format(t.get_video_id(), t.get_video_status())) 12 | download(url) 13 | count += 1 14 | if len(new) > 1 and count == len(new) - 1: 15 | # in case we submmit videos too fast 16 | time.sleep(20) 17 | 18 | failed = task.get_task_status(4) 19 | count = 0 20 | for t in failed: 21 | url = "https://www.youtube.com/watch?v={}".format(t.get_video_id()) 22 | print("[-] downloading {} | status: {}".format(t.get_video_id(), t.get_video_status())) 23 | download(url) 24 | count += 1 25 | if len(failed) > 1 and count == len(failed) - 1: 26 | # in case we submmit videos frequently 27 | time.sleep(20) 28 | 29 | schedule.every(5).minutes.do(download_from_tasks) 30 | 31 | while True: 32 | schedule.run_pending() 33 | time.sleep(1) -------------------------------------------------------------------------------- /src/manually_dl.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | parser = argparse.ArgumentParser() 3 | parser.add_argument("-d", "--db", help="use db", action='store_true') 4 | args = parser.parse_args() 5 | 6 | from modules.video import download 7 | print("url: ") 8 | url = input() 9 | if url.find("youtube.com/watch?v=") == -1 and url.find("youtu.be/") == -1: 10 | print("[!] not a valid youtube video link") 11 | exit() 12 | 13 | if url.find("youtube.com/watch?v=") != -1: 14 | id = url.replace("https://www.youtube.com/watch?v=", "") 15 | elif url.find("youtu.be/") != -1: 16 | id = url.replace("https://youtu.be/", "") 17 | 18 | download(url, id, args.db) -------------------------------------------------------------------------------- /src/modules/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | 3 | class config: 4 | # ConfigParser() doesn't work here because if the sessdata string contains '%' 5 | # it will throw this error: '%' must be followed by '%' or '(', found: "%&'" 6 | config = configparser.RawConfigParser() 7 | 8 | cookie_sessdata = "" 9 | cookie_jct = "" 10 | 11 | database_host = "" 12 | database_user = "" 13 | database_password = "" 14 | database_name = "" 15 | 16 | setup_time = "" 17 | 18 | bilibili_tid = 0 19 | bilibili_tag = "" 20 | bilibili_desc_len = 2000 21 | bilibili_video_info = False 22 | 23 | @classmethod 24 | def read(self): 25 | try: 26 | self.config.read("config.ini", encoding="utf-8") 27 | self.cookie_sessdata = self.config.get("cookie", "sessdata") 28 | self.cookie_jct = self.config.get("cookie", "bili_jct") 29 | self.database_host = self.config.get("database", "host") 30 | self.database_user = self.config.get("database", "user") 31 | self.database_password = self.config.get("database", "password") 32 | self.database_name = self.config.get("database", "name") 33 | self.setup_time = self.config.get("time", "setup_time") 34 | self.bilibili_tid = self.config.getint("bilibili", "tid") 35 | self.bilibili_tag = self.config.get("bilibili", "tag") 36 | self.bilibili_desc_len = self.config.getint("bilibili", "desc_len") 37 | self.bilibili_video_info = self.config.getboolean("bilibili", "video_info") 38 | except Exception as e: 39 | print(e.args) -------------------------------------------------------------------------------- /src/modules/database.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from sqlalchemy import Column, String, Integer, create_engine, insert 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from .config import config 6 | 7 | config.read() 8 | base = declarative_base() 9 | 10 | def create_session(user, password, host, database_name): 11 | try: 12 | engine = create_engine(f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{database_name}") 13 | se = sessionmaker(bind=engine, autocommit=True, autoflush=True) 14 | # https://farer.org/2017/10/28/sqlalchemy_scoped_session 15 | session = scoped_session(se) 16 | return session 17 | except: 18 | pass 19 | 20 | @enum.unique 21 | class status(enum.IntEnum): 22 | new, downloaded, uploaded, error = range(1, 5) 23 | 24 | class channel(base): 25 | __tablename__ = "channel" 26 | 27 | channel_id = Column(String(255), primary_key=True) 28 | subscribe_mode = Column(String(255)) 29 | type = Column(Integer) 30 | 31 | @classmethod 32 | def add_channel(cls, **kwargs): 33 | channel = cls(**kwargs) 34 | se.add(channel) 35 | 36 | class task(base): 37 | __tablename__ = "task" 38 | id = Column(String(255), primary_key=True) 39 | status = Column(Integer, default=status.new.value) 40 | 41 | @classmethod 42 | def add_task(cls, **kwargs): 43 | task = cls(**kwargs) 44 | se.add(task) 45 | 46 | @classmethod 47 | def get_task_status(cls, status): 48 | return se.query(cls).filter(cls.status == status).all() 49 | 50 | def get_video_id(self): 51 | return self.id 52 | 53 | def get_video_status(self): 54 | return self.status 55 | 56 | class channel_type(base): 57 | __tablename__ = "channel_type" 58 | row_ = Column(Integer, primary_key=True, autoincrement=True, default=1) 59 | tag = Column(String(255)) 60 | category_id = Column(Integer, default=0) 61 | description_length = Column(Integer, default=2000) 62 | 63 | 64 | se = create_session(config.database_user, config.database_password, config.database_host, config.database_name) -------------------------------------------------------------------------------- /src/modules/video.py: -------------------------------------------------------------------------------- 1 | import youtube_dl 2 | import os 3 | import requests 4 | from PIL import Image 5 | from bilibili_api import sync, video_uploader, Credential 6 | from .config import config 7 | from .database import se, task, status, channel, channel_type 8 | 9 | class dl_logger(object): 10 | def debug(self, msg): 11 | print(msg) 12 | #pass 13 | 14 | def warning(self, msg): 15 | pass 16 | 17 | def error(self, msg): 18 | print(msg) 19 | 20 | def remove_stuff(id): 21 | if os.path.exists(f"videos/{id}.mp4"): 22 | os.remove(f"videos/{id}.mp4") 23 | 24 | if os.path.exists(f"thumbnail/{id}.jpg"): 25 | os.remove(f"thumbnail/{id}.jpg") 26 | 27 | if os.path.exists(f"thumbnail/{id}.webp"): 28 | os.remove(f"thumbnail/{id}.webp") 29 | 30 | async def upload(url, title, description, video, thumbnail, description_length, tags, category_id): 31 | credential = Credential(sessdata=config.cookie_sessdata, bili_jct=config.cookie_jct) 32 | 33 | ''' 34 | { 35 | "copyright": "int, 投稿类型。1 自制,2 转载。", 36 | "source": "str, 视频来源。投稿类型为转载时注明来源,为原创时为空。", 37 | "desc": "str, 视频简介。", 38 | "desc_format_id": 0, 39 | "dynamic": "str, 动态信息。", 40 | "mission_id": "int, 参加活动 ID,若不参加不要提供该项", 41 | "interactive": 0, 42 | "open_elec": "int, 是否展示充电信息。1 为是,0 为否。", 43 | "no_reprint": "int, 显示未经作者授权禁止转载,仅当为原创视频时有效。1 为启用,0 为关闭。", 44 | "subtitles": { 45 | "lan": "str: 字幕投稿语言,不清楚作用请将该项设置为空", 46 | "open": "int: 是否启用字幕投稿,1 or 0" 47 | }, 48 | "tag": "str, 视频标签。使用英文半角逗号分隔的标签组。示例:标签1,标签2,标签3", 49 | "tid": "int, 分区ID。可以使用 channel 模块进行查询。", 50 | "title": "str: 视频标题", 51 | "up_close_danmaku": "bool, 是否关闭弹幕。", 52 | "up_close_reply": "bool, 是否关闭评论。", 53 | "dtime": "int?: 可选,定时发布时间戳(秒)" 54 | } 55 | ''' 56 | 57 | meta = { 58 | "copyright": 2, 59 | "source": url, 60 | "desc": description[0:description_length - len(url)], # x个字符限制,视频源链接还算进去就挺离谱的,但每个分区的限制长度不一样就更离谱了 61 | "desc_format_id": 0, 62 | "dynamic": "", 63 | "interactive": 0, 64 | "open_elec": 1, 65 | "no_reprint": 1, 66 | "subtitles": { 67 | "lan": "", 68 | "open": 0 69 | }, 70 | "tag": tags, 71 | "tid": category_id, 72 | "title": title, 73 | "up_close_danmaku": False, 74 | "up_close_reply": False 75 | } 76 | 77 | # 所有分区的视频标题应该都是80个字符吧..? 78 | page = video_uploader.VideoUploaderPage(path=video, title=title[0:80], description=description) 79 | uploader = video_uploader.VideoUploader([page], meta, credential, cover_path=thumbnail) 80 | 81 | @uploader.on("__ALL__") 82 | async def event(data): 83 | print(data) 84 | 85 | await uploader.start() 86 | 87 | def download(url, id_src, database=True): 88 | if len(url) == 0: 89 | return 90 | 91 | if database == True: 92 | if se.query(task).filter(task.id==id_src).first() is None: 93 | print(f"[-] {id_src} is not in database, adding.") 94 | task.add_task(id=id_src) 95 | 96 | if not os.path.exists("videos"): 97 | os.mkdir("videos") 98 | 99 | if not os.path.exists("thumbnail"): 100 | os.mkdir("thumbnail") 101 | 102 | options = { 103 | 'format': "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best", 104 | 'forcethumbnail': False, 105 | 'forcetitle': False, 106 | 'forcedescription': False, 107 | 'logger': dl_logger(), 108 | 'outtmpl': u'videos/%(id)s.%(ext)s', 109 | } 110 | 111 | print("[-] 开始下载视频及相关信息") 112 | 113 | with youtube_dl.YoutubeDL(options) as dl: 114 | try: 115 | dl.cache.remove() 116 | info = dl.extract_info(url, download=True) 117 | #print(info) 118 | title = info['title'] 119 | thumbnail_url = info['thumbnail'] 120 | description = info['description'] 121 | channel_id = info['channel_url'].replace("https://www.youtube.com/channel/", "") 122 | video_id = info['id'] 123 | 124 | if config.bilibili_video_info == True: 125 | upload_date = info['upload_date'] 126 | upload_date = upload_date[:4] + '-' + upload_date[4:6] + '-' + upload_date[6:8] 127 | uploader = info['uploader'] 128 | description = "频道:" + uploader + " 日期:" + upload_date + "\n" + description 129 | 130 | img_data = requests.get(thumbnail_url).content 131 | with open(f"thumbnail/{video_id}.webp", "wb") as handler: 132 | handler.write(img_data) 133 | 134 | im = Image.open(f"thumbnail/{video_id}.webp").convert("RGB") 135 | im.save(f"thumbnail/{video_id}.jpg", "jpeg") 136 | print("[+] 下载部分已完成") 137 | 138 | tags = config.bilibili_tag 139 | category_id = config.bilibili_tid 140 | description_length = config.bilibili_desc_len 141 | 142 | if database == True: 143 | print("[-] 获取频道信息中") 144 | query_result = se.query(channel).filter(channel.channel_id == channel_id).first() 145 | if query_result is not None: 146 | t = query_result.type 147 | type_info = se.query(channel_type).filter(channel_type.row_ == t).first() 148 | if type_info is not None: 149 | tags = type_info.tag 150 | category_id = type_info.category_id 151 | description_length = type_info.description_length 152 | else: 153 | raise Exception("找到了频道但找不到对应的类型,请更新表") 154 | else: 155 | print("[-] 无法从数据库里获取频道信息,使用配置文件里的上传信息(分区,标签,简介长度)") 156 | 157 | print("[-] 上传中") 158 | sync(upload(url=url, title=title, description=description, video=f"videos/{video_id}.mp4", thumbnail=f"thumbnail/{video_id}.jpg", tags=tags, category_id=category_id, description_length=description_length)) 159 | if database == True: 160 | se.query(task).filter(task.id == video_id).update({"status": status.uploaded.value}) 161 | 162 | print("[+] 上传成功") 163 | remove_stuff(video_id) 164 | except Exception as e: 165 | error_msg = e.__str__() 166 | if database == True: 167 | if error_msg.find("代码:21012") != -1: #消息:请不要反复提交相同标题的稿件(虽然我觉得不会有就是了... 168 | se.query(task).filter(task.id == video_id).update({"status": status.uploaded.value}) 169 | remove_stuff(video_id) 170 | else: 171 | se.query(task).filter(task.id == video_id).update({"status": status.error.value}) 172 | print("[!] 上传失败 原因:" + error_msg) 173 | -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- 1 | import feedparser 2 | from flask import Flask 3 | from flask import request 4 | from flask_sqlalchemy import SQLAlchemy 5 | from modules.config import config 6 | 7 | config.read() 8 | 9 | app = Flask(__name__) 10 | app.config["DEBUG"] = True 11 | app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://{}:{}@{}:3306/{}'.format(config.database_user, config.database_password, config.database_host, config.database_name) 12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 13 | 14 | db = SQLAlchemy() 15 | 16 | class channel(db.Model): 17 | channel_id = db.Column(db.String(255), primary_key=True) 18 | subscribe_mode = db.Column(db.String(255)) 19 | 20 | class tasks(db.Model): 21 | id = db.Column(db.String(255), primary_key=True) 22 | status = db.Column(db.Integer, default=1) 23 | 24 | db.init_app(app) 25 | db.create_all(app=app) 26 | 27 | @app.route("/feed/", methods=['GET', 'POST']) 28 | def feed(): 29 | if request.method == 'POST': 30 | feed = feedparser.parse(request.get_data(parse_form_data=True)) 31 | if feed: 32 | print('[-] Successfully parsed video feed.') 33 | for data in feed['entries']: 34 | video_id = data['yt_videoid'] 35 | if tasks.query.filter(tasks.id == video_id).first() is None: 36 | print(f"[!] Failed to find video id {video_id} in database, adding.") 37 | db.session.add(tasks(id=video_id)) 38 | db.session.commit() 39 | 40 | if request.method == 'GET': 41 | challenge = request.args.get('hub.challenge') 42 | mode = request.args.get('hub.mode') 43 | topic = request.args.get('hub.topic') 44 | 45 | if challenge and topic and mode: 46 | id = topic[-24:] 47 | query = channel.query.filter(channel.channel_id == id).first() 48 | 49 | if query is None: 50 | if mode == 'subscribe': 51 | print(f"[!] Failed to find channel id {id} in database, adding.") 52 | db.session.add(channel(channel_id=id, subscribe_mode=mode)) 53 | db.session.commit() 54 | else: 55 | if mode == 'unsubscribe': 56 | print(f"[!] Unsubscribe mode was found, deleting channel id {id} from database.") 57 | db.session.delete(query) 58 | db.session.commit() 59 | 60 | # returning challenge for authorization 61 | return challenge 62 | 63 | return '', 204 64 | 65 | app.run() --------------------------------------------------------------------------------