├── looplive ├── __init__.py ├── execute │ ├── __init__.py │ └── scan_and_execute.py ├── model │ ├── __init__.py │ └── model.py ├── tests │ ├── __init__.py │ └── test_execute.py ├── controller │ ├── __init__.py │ ├── bili_controller.py │ ├── ytb_controller.py │ └── config_controller.py ├── main.py └── cli.py ├── .dockerignore ├── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ ├── docs-feedback.md │ ├── feature_request.md │ ├── bug_report.md │ └── help-wanted.md └── pull_request_template.md ├── pyproject.toml ├── LICENSE ├── assets ├── youtube.svg └── bilibili.svg ├── README.md ├── README-en.md └── .gitignore /looplive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /looplive/execute/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /looplive/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /looplive/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /looplive/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | __pycache__/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | LABEL maintainer="timerring" 4 | 5 | WORKDIR /app 6 | 7 | COPY . /app 8 | 9 | RUN apt-get update && apt-get install -y ffmpeg \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | ENV TZ="Asia/Shanghai" 14 | 15 | CMD ["python", "-m", "looplive.main"] -------------------------------------------------------------------------------- /looplive/tests/test_execute.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import unittest 4 | from unittest.mock import patch, MagicMock 5 | from looplive.execute.scan_and_execute import scan_folder_and_execute 6 | 7 | class TestScanAndExecute(unittest.TestCase): 8 | 9 | def test_scan_folder_and_execute(self): 10 | folder_to_scan = "/home/Downloads/looplive/videos" 11 | command_to_execute = "ffmpeg -re -i {file_path} -c copy -f flv {stream_url}" 12 | scan_folder_and_execute(folder_to_scan, command_to_execute) -------------------------------------------------------------------------------- /looplive/execute/scan_and_execute.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import os 4 | import subprocess 5 | 6 | def scan_folder_and_execute(folder_path, stream_url): 7 | files = sorted(os.listdir(folder_path)) 8 | for file in files: 9 | if file.lower().endswith(('.flv', '.mp4')): 10 | file_path = os.path.join(folder_path, file) 11 | if os.path.isfile(file_path): 12 | command = f'ffmpeg -re -i {file_path} -c copy -f flv "{stream_url}"' 13 | subprocess.run(command, shell=True, check=True) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs-feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs feedback 3 | about: Improve documentation about this project. 4 | title: "[Docs] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Documentation Reference 11 | (Path/Link to the documentation file) 12 | 13 | ## Feedback on documentation 14 | (Your suggestions to the documentation. e.g., accuracy, complex explanations, structural organization, practical examples, technical reliability, and consistency) 15 | 16 | ## Additional context 17 | (Add any other context or screenshots about the documentation here.) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project. 4 | title: "[Feature] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | (A clear and concise description of what the problem is.) 12 | 13 | ## Describe the solution you'd like 14 | (A clear and concise description of what you want to happen.) 15 | 16 | ## Describe alternatives you've considered 17 | (A clear and concise description of any alternative solutions or features you've considered.) 18 | 19 | ## Additional context 20 | (Add any other context or screenshots about the feature request here.) 21 | -------------------------------------------------------------------------------- /looplive/controller/bili_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import subprocess 4 | from looplive.execute.scan_and_execute import scan_folder_and_execute 5 | from looplive.controller.config_controller import ConfigController 6 | 7 | class BiliController: 8 | def __init__(self, cc: ConfigController): 9 | self.server_url = cc.get_config()['bili_server_url'] 10 | self.key = cc.get_config()['bili_key'] 11 | self.folder = cc.get_config()['folder'] 12 | 13 | @property 14 | def stream_url(self): 15 | return f'{self.server_url}{self.key}' 16 | 17 | def stream(self): 18 | while True: 19 | scan_folder_and_execute(self.folder, self.stream_url) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "looplive" 7 | version = "0.0.2" 8 | authors = [ 9 | { name="timerring"}, 10 | ] 11 | description = "The Python toolkit package and cli designed for auto loop live." 12 | readme = "README.md" 13 | license = { file="LICENSE" } 14 | requires-python = ">=3.6" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [] 21 | 22 | [project.scripts] 23 | looplive = "looplive.cli:cli" 24 | 25 | [project.urls] 26 | "Homepage" = "https://github.com/timerring/looplive" 27 | -------------------------------------------------------------------------------- /looplive/controller/ytb_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import subprocess 4 | from looplive.execute.scan_and_execute import scan_folder_and_execute 5 | from looplive.controller.config_controller import ConfigController 6 | 7 | class YtbController: 8 | def __init__(self, cc: ConfigController): 9 | self.server_url = cc.get_config()['ytb_server_url'] 10 | self.key = cc.get_config()['ytb_key'] 11 | self.folder = cc.get_config()['folder'] 12 | 13 | @property 14 | def stream_url(self): 15 | # Add leading slash to key if it doesn't start with one 16 | key = self.key if self.key.startswith('/') else f'/{self.key}' 17 | return f'{self.server_url}{key}' 18 | 19 | def stream(self): 20 | while True: 21 | scan_folder_and_execute(self.folder, self.stream_url) -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | [Please describe the background, purpose, changes made, and how to test this PR] 5 | 6 | ## Related Issues 7 | 8 | [List the issue numbers related to this PR] 9 | 10 | ## Changes Proposed 11 | 12 | - [ ] change1 13 | - [ ] ... 14 | 15 | ## Who Can Review? 16 | 17 | [Please use the '@' symbol to mention any community member who is free to review the PR once the tests have passed. Feel free to tag members or contributors who might be interested in your PR.] 18 | 19 | ## TODO 20 | 21 | - [ ] task1 22 | - [ ] ... 23 | 24 | ## Checklist 25 | 26 | - [ ] Code has been reviewed 27 | - [ ] Code complies with the project's code standards and best practices 28 | - [ ] Code has passed all tests 29 | - [ ] Code does not affect the normal use of existing features 30 | - [ ] Code has been commented properly 31 | - [ ] Documentation has been updated (if applicable) 32 | - [ ] Demo/checkpoint has been attached (if applicable) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve this project. 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | (A clear and concise description of what the bug is.) 12 | 13 | ## How To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Config/File changes: ... 16 | 2. Run command: ... 17 | 3. See error: ... 18 | 19 | ## Expected behavior 20 | (A clear and concise description of what you expected to happen.) 21 | 22 | ## Screenshots 23 | (If applicable, add screenshots to help explain your problem.) 24 | 25 | ## Environment Information 26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS] 27 | - Python Version: [e.g. Python 3.9.15] 28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4] 29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs] 30 | 31 | ## Additional context 32 | (Add any other context about the problem here.) 33 | -------------------------------------------------------------------------------- /looplive/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from looplive.model.model import Model 4 | from looplive.controller.config_controller import ConfigController 5 | from looplive.controller.bili_controller import BiliController 6 | from looplive.controller.ytb_controller import YtbController 7 | if __name__ == '__main__': 8 | path = os.path.join(os.path.dirname(__file__), 'model/config.json') 9 | cc = ConfigController(path=path) 10 | if cc.check_bili_config() and cc.check_ytb_config(): 11 | bili_thread = threading.Thread(target=lambda: BiliController(cc).stream()) 12 | ytb_thread = threading.Thread(target=lambda: YtbController(cc).stream()) 13 | 14 | bili_thread.start() 15 | ytb_thread.start() 16 | 17 | bili_thread.join() 18 | ytb_thread.join() 19 | elif cc.check_bili_config(): 20 | BiliController(cc).stream() 21 | elif cc.check_ytb_config(): 22 | YtbController(cc).stream() 23 | else: 24 | print("Please complete the configuration first!") -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help wanted 3 | about: Want help from this project team. 4 | title: "[Help] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Problem Overview 11 | (Briefly and clearly describe the issue you're facing and seeking help with.) 12 | 13 | ## Steps Taken 14 | (Detail your attempts to resolve the issue, including any relevant steps or processes.) 15 | 1. Config/File changes: ... 16 | 2. Run command: ... 17 | 3. See errors: ... 18 | 19 | ## Expected Outcome 20 | (A clear and concise description of what you expected to happen.) 21 | 22 | ## Screenshots 23 | (If applicable, add screenshots to help explain your problem.) 24 | 25 | ## Environment Information 26 | - Operating System: [e.g. Ubuntu 20.04.5 LTS] 27 | - Python Version: [e.g. Python 3.9.15] 28 | - Driver & CUDA Version: [e.g. Driver 470.103.01 & CUDA 11.4] 29 | - Error Messages and Logs: [If applicable, provide any error messages or relevant log outputs] 30 | 31 | ## Additional context 32 | (Add any other context about the problem here.) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 John Howe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /looplive/model/model.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import json 4 | import os 5 | 6 | class Model: 7 | def __init__(self, path=None) -> None: 8 | if path is None: 9 | self.path = os.path.join(os.path.dirname(__file__), "config.json") 10 | else: 11 | self.path = path 12 | self.default_config = { 13 | "folder": "", 14 | "bili_server_url": "", 15 | "bili_key": "", 16 | "ytb_server_url": "", 17 | "ytb_key": "" 18 | } 19 | 20 | def get_default_config(self): 21 | return self.default_config 22 | 23 | def reset_config(self): 24 | self.write(self.default_config) 25 | 26 | def update_specific_config(self, key, value): 27 | config_info = self.get_config() 28 | config_info[key] = value 29 | self.write(config_info) 30 | 31 | def update_multiple_config(self, updates: dict): 32 | config_info = self.get_config() 33 | for key, value in updates.items(): 34 | config_info[key] = value 35 | self.write(config_info) 36 | 37 | def get_config(self): 38 | if not os.path.exists(self.path): 39 | self.reset_config() 40 | return self.read() 41 | 42 | def read(self): 43 | with open(self.path, "r") as f: 44 | return json.load(f) 45 | 46 | def write(self, config): 47 | with open(self.path, "w") as f: 48 | json.dump(config, f, indent=4) 49 | -------------------------------------------------------------------------------- /looplive/controller/config_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | from looplive.model.model import Model 4 | 5 | class ConfigController: 6 | def __init__(self, path=None): 7 | self.model = Model(path=path) 8 | 9 | def check_folder_config(self): 10 | config_info = self.model.get_config() 11 | if not config_info['folder']: 12 | print("The folder is not complete!", flush=True) 13 | return False 14 | return True 15 | 16 | def check_bili_config(self): 17 | config_info = self.model.get_config() 18 | if not self.check_folder_config() or not config_info['bili_server_url'] or not config_info['bili_key']: 19 | print("The bilibili configuration is not complete!", flush=True) 20 | return False 21 | return True 22 | 23 | def check_ytb_config(self): 24 | config_info = self.model.get_config() 25 | if not self.check_folder_config() or not config_info['ytb_server_url'] or not config_info['ytb_key']: 26 | print("The youtube configuration is not complete!", flush=True) 27 | return False 28 | return True 29 | 30 | def update_config(self, config_info): 31 | self.model.update_multiple_config(config_info) 32 | 33 | def reset_config(self): 34 | self.model.reset_config() 35 | 36 | def get_config(self): 37 | return self.model.get_config() 38 | 39 | def get_specific_config(self, key): 40 | return self.model.get_specific_config(key) 41 | 42 | def update_specific_config(self, key, value): 43 | self.model.update_specific_config(key, value) 44 | -------------------------------------------------------------------------------- /assets/youtube.svg: -------------------------------------------------------------------------------- 1 | Youtube Streamline Icon: https://streamlinehq.com -------------------------------------------------------------------------------- /assets/bilibili.svg: -------------------------------------------------------------------------------- 1 | Bilibili Logo Streamline Icon: https://streamlinehq.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | bilitool 5 | 6 |

7 | 8 | 简体中文 | [English](./README-en.md) 9 | 10 | `looplive` 是一个 7 x 24 小时全自动**循环多平台同时推流**直播工具。 11 | 12 | 支持平台(支持更多平台欢迎[issue](https://github.com/timerring/looplive/issues)) 13 | 14 | Bilibili 15 | Youtube 16 | 17 |
18 | 19 | ## 特点 20 | 21 | > 欢迎使用,欢迎提供更多反馈,欢迎 PR 贡献此项目,请勿用于违反社区规定的用途。 22 | 23 | - 支持 7 x 24 小时**全自动循环直播**推流 24 | - 支持多平台**同时直播**: 25 | - Bilibili 26 | - Youtube 27 | - 理论上只要平台有 rtmp 服务器就能支持,增加新的平台欢迎 [issue](https://github.com/timerring/looplive/issues)。 28 | - 支持记忆参数,**仅需添加一次**,后续一键自动运行 29 | - 支持 Docker 部署 30 | 31 | ## 提前准备 32 | 33 | 使用此工具前,您需要先安装ffmpeg: 34 | 35 | - Windows: `choco install ffmpeg`(通过[Chocolatey](https://chocolatey.org/))或其他方法 36 | - macOS: `brew install ffmpeg`(通过[Homebrew](https://brew.sh/)) 37 | - Linux: `sudo apt install ffmpeg`(Debian/Ubuntu) 38 | 39 | 更多操作系统安装 ffmpeg 请参考[官方网站](https://ffmpeg.org/download.html)。 40 | 41 | 然后安装 looplive 42 | 43 | ```bash 44 | pip install looplive 45 | ``` 46 | 47 | ## 快速开始 48 | 49 | ### 开始直播 50 | 51 | #### Bilibili 52 | 53 | 1. 前往 [直播页面](https://link.bilibili.com/p/center/index#/my-room/start-live). 54 | - 如果你还没有直播权限,请先申请,点击 `立即开通直播间`,然后按照 b 站提示操作。 55 | 2. 点击 `开始直播`. 56 | 3. 获取推流服务器地址 `-bs` 和串流密钥 `-bk`,如下图所示,直接复制即可。 57 | 58 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。 59 | 60 | ![bilibili](https://cdn.jsdelivr.net/gh/timerring/scratchpad2023/2024/2025-03-28-22-59-03.png) 61 | 62 | #### Youtube 63 | 64 | 1. 前往 [直播页面](https://www.youtube.com/live_dashboard). 65 | 2. 获取推流服务器地址(Stream URL) `-ys` 和串流密钥(Stream key) `-yk`。 66 | 67 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。 68 | 69 | ![youtube](https://cdn.jsdelivr.net/gh/timerring/scratchpad2023/2024/2025-03-28-22-13-59.png) 70 | 71 | #### 视频文件夹 72 | 73 | **参数 `-f` 是视频文件的存放文件夹** 74 | 75 | > 为了避免命令参数被错误分隔,请使用英文双引号 `"` 包裹每一项参数。可以只填需要的平台对应的 server_url 和 key。 76 | 77 | ```bash 78 | # eg. 79 | # 只需要推流到 Bilibili 的配置,只需添加一次,例如 80 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -f "your/folder/path" 81 | # 只需要推流到 Youtube 的配置,只需添加一次 82 | looplive add -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path" 83 | # 同时推流到 Bilibili 和 Youtube 的配置,只需添加一次 84 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path" 85 | ``` 86 | 87 | ### 推流 88 | 89 | 只需添加一次参数,以后直接执行以下命令启动即可。 90 | 91 | ```bash 92 | # 只推流到 Bilibili 93 | looplive bili 94 | # 只推流到 Youtube 95 | looplive youtube 96 | # 同时推流到 Bilibili 和 Youtube 97 | looplive both 98 | ``` 99 | 100 | ## Docker 部署 101 | 102 | ### 配置文件 103 | 104 | ```json 105 | { 106 | "folder": "/app/looplive/videos", // 由于 docker 的挂载映射,最好不要修改这里 107 | "bili_server_url": "rtmp://live-push.bilivideo.com/live-bvc/", 108 | "bili_key": "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1", 109 | "ytb_server_url": "rtmp://a.rtmp.youtube.com/live2", // 不需要可置为空 "" 110 | "ytb_key": "ghkh-sfgg-loop-live-live" // 不需要可置为空 "" 111 | } 112 | ``` 113 | 114 | ### 运行 115 | 116 | ```bash 117 | sudo docker run -it \ 118 | -v /your/path/to/config.json:/app/looplive/model/config.json \ 119 | -v /your/path/to/videos:/app/looplive/videos \ 120 | --name looplive_docker \ 121 | ghcr.io/timerring/looplive:0.0.2 122 | ``` 123 | 124 | ### 更多用法 125 | 126 | ```bash 127 | $ looplive -h 128 | 129 | looplive [-h] [-V] {check,add,reset,bili} ... 130 | 131 | 132 | The Python toolkit package and cli designed for auto loop live. 133 | 134 | positional arguments: 135 | {check,add,reset,bili,ytb,both} 136 | Subcommands 137 | check Check the configuration 138 | add Add the configuration 139 | reset Reset the configuration 140 | bili Stream on the bilibili platform 141 | ytb Stream on the youtube platform 142 | both Stream on the bilibili and youtube platform 143 | 144 | options: 145 | -h, --help show this help message and exit 146 | -V, --version Print version information 147 | ``` -------------------------------------------------------------------------------- /looplive/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 looplive 2 | 3 | import argparse 4 | import sys 5 | import os 6 | import logging 7 | import threading 8 | import textwrap 9 | from looplive.controller.bili_controller import BiliController 10 | from looplive.controller.ytb_controller import YtbController 11 | from looplive.controller.config_controller import ConfigController 12 | 13 | 14 | def cli(): 15 | parser = argparse.ArgumentParser( 16 | prog="looplive", 17 | formatter_class=argparse.RawDescriptionHelpFormatter, 18 | description=textwrap.dedent(''' 19 | The Python toolkit package and cli designed for auto loop live. 20 | Source code at https://github.com/timerring/looplive 21 | '''), 22 | epilog=textwrap.dedent(''' 23 | Example: 24 | looplive check 25 | looplive add -bs "rtmp://xxx" -bk "?streamname=xxx" -ys "rtmp://xxx" -yk "xxx-xxx-xxx-xxx" -f "your/folder/path" 26 | looplive bili 27 | looplive ytb 28 | looplive both 29 | '''), 30 | ) 31 | parser.add_argument( 32 | "-V", 33 | "--version", 34 | action="version", 35 | version="looplive 0.0.2 and source code at https://github.com/timerring/looplive", 36 | help="Print version information", 37 | ) 38 | 39 | subparsers = parser.add_subparsers(dest='subcommand', help='Subcommands') 40 | 41 | # Check config 42 | check_config_parser = subparsers.add_parser('check', help='Check the configuration') 43 | 44 | # Add config 45 | add_config_parser = subparsers.add_parser('add', help='Add the configuration') 46 | add_config_parser.add_argument('-bs', '--bili_server_url', help='The bilibili server url') 47 | add_config_parser.add_argument('-bk', '--bili_key', help='The bilibili stream key') 48 | add_config_parser.add_argument('-ys', '--ytb_server_url', help='The youtube server url') 49 | add_config_parser.add_argument('-yk', '--ytb_key', help='The youtube stream key') 50 | add_config_parser.add_argument('-f', '--folder', help='The input folder') 51 | 52 | # Reset config 53 | reset_config_parser = subparsers.add_parser('reset', help='Reset the configuration') 54 | 55 | # Bilibili stream 56 | bilibili_parser = subparsers.add_parser('bili', help='Stream on the bilibili platform') 57 | 58 | # Youtube stream 59 | youtube_parser = subparsers.add_parser('ytb', help='Stream on the youtube platform') 60 | 61 | # Both stream 62 | both_parser = subparsers.add_parser('both', help='Stream on the bilibili and youtube platform') 63 | 64 | args = parser.parse_args() 65 | 66 | # Check if no subcommand is provided 67 | if args.subcommand is None: 68 | print("No subcommand provided. Please specify a subcommand.") 69 | parser.print_help() 70 | sys.exit() 71 | 72 | if args.subcommand == 'check': 73 | print(ConfigController().get_config()) 74 | 75 | if args.subcommand == 'add': 76 | if args.bili_server_url: 77 | ConfigController().update_specific_config('bili_server_url', args.bili_server_url) 78 | if args.bili_key: 79 | ConfigController().update_specific_config('bili_key', args.bili_key) 80 | if args.ytb_server_url: 81 | ConfigController().update_specific_config('ytb_server_url', args.ytb_server_url) 82 | if args.ytb_key: 83 | ConfigController().update_specific_config('ytb_key', args.ytb_key) 84 | if args.folder: 85 | ConfigController().update_specific_config('folder', args.folder) 86 | 87 | if args.subcommand == 'reset': 88 | ConfigController().reset_config() 89 | 90 | if args.subcommand == 'bili': 91 | cc = ConfigController() 92 | if cc.check_bili_config(): 93 | BiliController(cc).stream() 94 | else: 95 | print("Please complete the bilibili configuration first!", flush=True) 96 | 97 | if args.subcommand == 'ytb': 98 | cc = ConfigController() 99 | if cc.check_ytb_config(): 100 | YtbController(cc).stream() 101 | else: 102 | print("Please complete the youtube configuration first!", flush=True) 103 | 104 | if args.subcommand == 'both': 105 | cc = ConfigController() 106 | if cc.check_bili_config() and cc.check_ytb_config(): 107 | # two threads 108 | bili_thread = threading.Thread(target=lambda: BiliController(cc).stream()) 109 | ytb_thread = threading.Thread(target=lambda: YtbController(cc).stream()) 110 | 111 | bili_thread.start() 112 | ytb_thread.start() 113 | 114 | bili_thread.join() 115 | ytb_thread.join() 116 | else: 117 | print("Please complete the configuration first!", flush=True) 118 | 119 | if __name__ == '__main__': 120 | cli() -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | bilitool 5 | 6 |

7 | 8 | [简体中文](./README.md) | English 9 | 10 | `looplive` is a 24/7 automated **multi-platform simultaneous streaming** tool. 11 | 12 | Supported Platforms (support more platforms welcome via [issue](https://github.com/timerring/looplive/issues)) 13 | 14 | Bilibili 15 | Youtube 16 | 17 |
18 | 19 | ## Features 20 | 21 | > Welcome to use, feedback is appreciated, PRs are welcome. Please do not use for purposes that violate community guidelines. 22 | 23 | - Supports 24/7 **automated loop streaming** 24 | - Supports **simultaneous streaming** to multiple platforms: 25 | - Bilibili 26 | - Youtube 27 | - Theoretically supports any platform with RTMP servers, more platforms welcome via [issue](https://github.com/timerring/looplive/issues) 28 | - Remembers parameters, **configure once**, run automatically afterwards 29 | - Supports Docker deployment 30 | 31 | ## Prerequisites 32 | 33 | Before using this tool, you need to install ffmpeg: 34 | 35 | - Windows: `choco install ffmpeg` (via [Chocolatey](https://chocolatey.org/)) or other methods 36 | - macOS: `brew install ffmpeg` (via [Homebrew](https://brew.sh/)) 37 | - Linux: `sudo apt install ffmpeg` (Debian/Ubuntu) 38 | 39 | For other operating systems, please refer to the [official website](https://ffmpeg.org/download.html). 40 | 41 | Then install looplive: 42 | 43 | ```bash 44 | pip install looplive 45 | ``` 46 | 47 | ## Quick Start 48 | 49 | ### Start Streaming 50 | 51 | #### Bilibili 52 | 53 | 1. Go to the [live streaming page](https://link.bilibili.com/p/center/index#/my-room/start-live). 54 | - If you don't have streaming permissions yet, apply first by clicking `Start Live Room` and follow Bilibili's instructions. 55 | 2. Click `Start Live Streaming`. 56 | 3. Get the streaming server URL `-bs` and stream key `-bk` as shown in the image below, just copy them directly. 57 | 58 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`. 59 | 60 | ![bilibili](https://cdn.jsdelivr.net/gh/timerring/scratchpad2023/2024/2025-03-28-22-59-03.png) 61 | 62 | #### Youtube 63 | 64 | 1. Go to the [live streaming page](https://www.youtube.com/live_dashboard). 65 | 2. Get the Stream URL `-ys` and Stream key `-yk`. 66 | 67 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`. 68 | 69 | ![youtube](https://cdn.jsdelivr.net/gh/timerring/scratchpad2023/2024/2025-03-28-22-13-59.png) 70 | 71 | #### Video Folder 72 | 73 | **Parameter `-f` is the folder where video files are stored** 74 | 75 | > To avoid command parameter parsing issues, please wrap each parameter in English double quotes `"`. You only need to fill in the server_url and key for the platforms you want to use. 76 | 77 | ```bash 78 | # eg. 79 | # Configuration for Bilibili only, add once 80 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -f "your/folder/path" 81 | # Configuration for Youtube only, add once 82 | looplive add -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path" 83 | # Configuration for both Bilibili and Youtube, add once 84 | looplive add -bs "rtmp://live-push.bilivideo.com/live-bvc/" -bk "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1" -ys "rtmp://a.rtmp.youtube.com/live2" -yk "ghkh-sfgg-loop-live-live" -f "your/folder/path" 85 | ``` 86 | 87 | ### Streaming 88 | 89 | After adding parameters once, you can start streaming with the following commands. 90 | 91 | ```bash 92 | # Stream to Bilibili only 93 | looplive bili 94 | # Stream to Youtube only 95 | looplive youtube 96 | # Stream to both Bilibili and Youtube 97 | looplive both 98 | ``` 99 | 100 | ## Docker Deployment 101 | 102 | ### Configuration File 103 | 104 | 105 | ```json 106 | { 107 | "folder": "/app/looplive/videos", // don't change this 108 | "bili_server_url": "rtmp://live-push.bilivideo.com/live-bvc/", 109 | "bili_key": "?streamname=live_3541234541234567_8901234&key=looplivexxxxxxxxxxxxdgd&schedule=rtmp&pflag=1", 110 | "ytb_server_url": "rtmp://a.rtmp.youtube.com/live2", // Set to "" if not needed 111 | "ytb_key": "ghkh-sfgg-loop-live-live" // Set to "" if not needed 112 | } 113 | ``` 114 | 115 | ### Running 116 | 117 | ```bash 118 | sudo docker run -it \ 119 | -v /your/path/to/config.json:/app/looplive/model/config.json \ 120 | -v /your/path/to/videos:/app/looplive/videos \ 121 | --name looplive_docker \ 122 | ghcr.io/timerring/looplive:0.0.2 123 | ``` 124 | 125 | ### More Usage 126 | 127 | ```bash 128 | $ looplive -h 129 | 130 | looplive [-h] [-V] {check,add,reset,bili} ... 131 | 132 | 133 | The Python toolkit package and cli designed for auto loop live. 134 | 135 | positional arguments: 136 | {check,add,reset,bili,ytb,both} 137 | Subcommands 138 | check Check the configuration 139 | add Add the configuration 140 | reset Reset the configuration 141 | bili Stream on the bilibili platform 142 | ytb Stream on the youtube platform 143 | both Stream on the bilibili and youtube platform 144 | 145 | options: 146 | -h, --help show this help message and exit 147 | -V, --version Print version information 148 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,python,pycharm+all,macos,windows 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,python,pycharm+all,macos,windows 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/artifacts 36 | # .idea/compiler.xml 37 | # .idea/jarRepositories.xml 38 | # .idea/modules.xml 39 | # .idea/*.iml 40 | # .idea/modules 41 | # *.iml 42 | # *.ipr 43 | 44 | # CMake 45 | cmake-build-*/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Cursive Clojure plugin 63 | .idea/replstate.xml 64 | 65 | # Crashlytics plugin (for Android Studio and IntelliJ) 66 | com_crashlytics_export_strings.xml 67 | crashlytics.properties 68 | crashlytics-build.properties 69 | fabric.properties 70 | 71 | # Editor-based Rest Client 72 | .idea/httpRequests 73 | 74 | # Android studio 3.1+ serialized cache file 75 | .idea/caches/build_file_checksums.ser 76 | 77 | ### Intellij+all Patch ### 78 | # Ignores the whole .idea folder and all .iml files 79 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 80 | 81 | .idea/ 82 | 83 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 84 | 85 | *.iml 86 | modules.xml 87 | .idea/misc.xml 88 | *.ipr 89 | 90 | # Sonarlint plugin 91 | .idea/sonarlint 92 | 93 | ### macOS ### 94 | # General 95 | .DS_Store 96 | .AppleDouble 97 | .LSOverride 98 | 99 | # Icon must end with two \r 100 | Icon 101 | 102 | 103 | # Thumbnails 104 | ._* 105 | 106 | # Files that might appear in the root of a volume 107 | .DocumentRevisions-V100 108 | .fseventsd 109 | .Spotlight-V100 110 | .TemporaryItems 111 | .Trashes 112 | .VolumeIcon.icns 113 | .com.apple.timemachine.donotpresent 114 | 115 | # Directories potentially created on remote AFP share 116 | .AppleDB 117 | .AppleDesktop 118 | Network Trash Folder 119 | Temporary Items 120 | .apdisk 121 | 122 | ### PyCharm+all ### 123 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 124 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 125 | 126 | # User-specific stuff 127 | 128 | # Generated files 129 | 130 | # Sensitive or high-churn files 131 | 132 | # Gradle 133 | 134 | # Gradle and Maven with auto-import 135 | # When using Gradle or Maven with auto-import, you should exclude module files, 136 | # since they will be recreated, and may cause churn. Uncomment if using 137 | # auto-import. 138 | # .idea/artifacts 139 | # .idea/compiler.xml 140 | # .idea/jarRepositories.xml 141 | # .idea/modules.xml 142 | # .idea/*.iml 143 | # .idea/modules 144 | # *.iml 145 | # *.ipr 146 | 147 | # CMake 148 | 149 | # Mongo Explorer plugin 150 | 151 | # File-based project format 152 | 153 | # IntelliJ 154 | 155 | # mpeltonen/sbt-idea plugin 156 | 157 | # JIRA plugin 158 | 159 | # Cursive Clojure plugin 160 | 161 | # Crashlytics plugin (for Android Studio and IntelliJ) 162 | 163 | # Editor-based Rest Client 164 | 165 | # Android studio 3.1+ serialized cache file 166 | 167 | ### PyCharm+all Patch ### 168 | # Ignores the whole .idea folder and all .iml files 169 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 170 | 171 | 172 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 173 | 174 | 175 | # Sonarlint plugin 176 | 177 | ### Python ### 178 | # Byte-compiled / optimized / DLL files 179 | __pycache__/ 180 | *.py[cod] 181 | *$py.class 182 | 183 | # C extensions 184 | *.so 185 | 186 | # Distribution / packaging 187 | .Python 188 | build/ 189 | develop-eggs/ 190 | dist/ 191 | downloads/ 192 | eggs/ 193 | .eggs/ 194 | parts/ 195 | sdist/ 196 | var/ 197 | wheels/ 198 | pip-wheel-metadata/ 199 | share/python-wheels/ 200 | *.egg-info/ 201 | .installed.cfg 202 | *.egg 203 | MANIFEST 204 | 205 | # PyInstaller 206 | # Usually these files are written by a python script from a template 207 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 208 | *.manifest 209 | *.spec 210 | 211 | # Installer logs 212 | pip-log.txt 213 | pip-delete-this-directory.txt 214 | 215 | # Unit test / coverage reports 216 | htmlcov/ 217 | .tox/ 218 | .nox/ 219 | .coverage 220 | .coverage.* 221 | .cache 222 | nosetests.xml 223 | coverage.xml 224 | *.cover 225 | *.py,cover 226 | .hypothesis/ 227 | .pytest_cache/ 228 | pytestdebug.log 229 | 230 | # Translations 231 | *.mo 232 | *.pot 233 | 234 | # Django stuff: 235 | *.log 236 | local_settings.py 237 | db.sqlite3 238 | db.sqlite3-journal 239 | 240 | # Flask stuff: 241 | instance/ 242 | .webassets-cache 243 | 244 | # Scrapy stuff: 245 | .scrapy 246 | 247 | # Sphinx documentation 248 | docs/_build/ 249 | doc/_build/ 250 | 251 | # PyBuilder 252 | target/ 253 | 254 | # Jupyter Notebook 255 | .ipynb_checkpoints 256 | 257 | # IPython 258 | profile_default/ 259 | ipython_config.py 260 | 261 | # pyenv 262 | .python-version 263 | 264 | # pipenv 265 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 266 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 267 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 268 | # install all needed dependencies. 269 | #Pipfile.lock 270 | 271 | # poetry 272 | #poetry.lock 273 | 274 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 275 | __pypackages__/ 276 | 277 | # Celery stuff 278 | celerybeat-schedule 279 | celerybeat.pid 280 | 281 | # SageMath parsed files 282 | *.sage.py 283 | 284 | # Environments 285 | # .env 286 | .env/ 287 | .venv/ 288 | env/ 289 | venv/ 290 | ENV/ 291 | env.bak/ 292 | venv.bak/ 293 | pythonenv* 294 | 295 | # Spyder project settings 296 | .spyderproject 297 | .spyproject 298 | 299 | # Rope project settings 300 | .ropeproject 301 | 302 | # mkdocs documentation 303 | /site 304 | 305 | # mypy 306 | .mypy_cache/ 307 | .dmypy.json 308 | dmypy.json 309 | 310 | # Pyre type checker 311 | .pyre/ 312 | 313 | # pytype static type analyzer 314 | .pytype/ 315 | 316 | # operating system-related files 317 | *.DS_Store #file properties cache/storage on macOS 318 | Thumbs.db #thumbnail cache on Windows 319 | 320 | # profiling data 321 | .prof 322 | 323 | 324 | ### Windows ### 325 | # Windows thumbnail cache files 326 | Thumbs.db 327 | Thumbs.db:encryptable 328 | ehthumbs.db 329 | ehthumbs_vista.db 330 | 331 | # Dump file 332 | *.stackdump 333 | 334 | # Folder config file 335 | [Dd]esktop.ini 336 | 337 | # Recycle Bin used on file shares 338 | $RECYCLE.BIN/ 339 | 340 | # Windows Installer files 341 | *.cab 342 | *.msi 343 | *.msix 344 | *.msm 345 | *.msp 346 | 347 | # Windows shortcuts 348 | *.lnk 349 | 350 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all,python,pycharm+all,macos,windows 351 | 352 | # Specific for the MacOS 353 | .DS_Store 354 | 355 | # Specific for the looplive 356 | videos/* --------------------------------------------------------------------------------