first commit
This commit is contained in:
124
.gitignore
vendored
Normal file
124
.gitignore
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
.vscode/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM ubuntu:18.04 as builder
|
||||
|
||||
ARG VERSION="3.1.49_ubuntu_18.04"
|
||||
RUN apt-get update &&\
|
||||
apt-get install --no-install-recommends --yes wget && \
|
||||
apt-get clean && \
|
||||
rm --force --recursive /var/lib/apt/lists
|
||||
RUN mkdir /opt/acestream && \
|
||||
wget --no-check-certificate -O- "https://download.acestream.media/linux/acestream_3.1.49_ubuntu_18.04_x86_64.tar.gz" | \
|
||||
tar -xz -C /opt/acestream
|
||||
|
||||
# actual image
|
||||
FROM ubuntu:18.04
|
||||
LABEL maintainer="Jack Liar <zhigu1017@gmail.com>"
|
||||
RUN apt-get update --yes && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
apt-utils python-setuptools python-m2crypto python-apsw libpython2.7 libssl1.0.0 net-tools libxslt1.1 && \
|
||||
apt-get clean && \
|
||||
rm --force --recursive /var/lib/apt/lists
|
||||
COPY --from=builder /opt/acestream /opt/acestream
|
||||
|
||||
EXPOSE 6878
|
||||
|
||||
ENV ACESTREAM_ROOT="/opt/acestream"
|
||||
ENV LD_LIBRARY_PATH="${ACESTREAM_ROOT}/lib"
|
||||
|
||||
CMD ["/opt/acestream/acestreamengine", "--client-console"]
|
||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Jack Liar(https://github.com/JackLiar)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Docker Ace Stream server
|
||||
An [Ace Stream](http://www.acestream.org/) server Docker image & Python3 client to stream.
|
||||
- [Overview](#overview)
|
||||
- [Building](#building)
|
||||
- [Usage](#usage)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
What this provides:
|
||||
- Dockerized Ace Stream server (version `3.1.35`) running under Ubuntu 18.04.
|
||||
- Bash script to start server and present HTTP API endpoint to host.
|
||||
- Python playback script [`playstream.py`](playstream.py) instructing server to:
|
||||
- Commence streaming of a given content ID.
|
||||
- ...and optionally start a compatible media player (such as [iina(OS X only)](https://iina.io/)) to view stream.
|
||||
|
||||
Since a single HTTP endpoint exposed from the Docker container controls the server _and_ provides the output stream, this provides one of the easier methods for playback of Ace Streams on traditionally unsupported operating systems such as OS X.
|
||||
|
||||
## Building
|
||||
To build Docker image:
|
||||
```sh
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
Start the server via:
|
||||
```sh
|
||||
$ ./run.sh
|
||||
```
|
||||
|
||||
Server will now be available from `http://127.0.0.1:6878`:
|
||||
```sh
|
||||
$ curl http://127.0.0.1:6878/webui/api/service?method=get_version&format=jsonp&callback=
|
||||
# {"result": {"code": 3013500, "platform": "linux", "version": "3.1.35"}, "error": null}
|
||||
```
|
||||
|
||||
A program ID can be started with [`playstream.py`](playstream.py):
|
||||
```sh
|
||||
$ ./playstream.py --help
|
||||
usage: playstream.py [-h] --content-id HASH [--player PLAYER]
|
||||
[--server HOSTNAME] [--port PORT] [--multi-players] [-d]
|
||||
|
||||
Instructs server to commence a given content ID. Will execute a local media
|
||||
player once playback has started.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--content-id HASH content ID to stream
|
||||
--player PLAYER media player to execute once stream active
|
||||
--server HOSTNAME server hostname, defaults to 127.0.0.1
|
||||
--port PORT server HTTP API port, defaults to 6878
|
||||
--multi-players play stream in multiple players mode, defaults to False
|
||||
-d, --debug run client in debug mode
|
||||
```
|
||||
|
||||
For example, to stream `CONTENT_ID` and send playback to `iina` when ready:
|
||||
```sh
|
||||
$ ./playstream.py \
|
||||
--content-id CONTENT_ID \
|
||||
--player /usr/bin/vlc \
|
||||
|
||||
INFO 2019-05-12 18:45:52,190 playstream.py 47 Client starts.
|
||||
INFO 2019-05-12 18:45:52,202 playstream.py 91 acestream engine version: 3.1.35
|
||||
INFO 2019-05-12 18:45:52,202 playstream.py 93 acestream engine version code: 3013500
|
||||
INFO 2019-05-12 18:45:52,202 playstream.py 188 Acestream server is available
|
||||
Status: | Peers: 0 | Down: 0KB/s | Up: 0KB/s
|
||||
Status: prebuf | Peers: 0 | Down: 0KB/s | Up: 0KB/s
|
||||
Status: prebuf | Peers: 4 | Down: 24KB/s | Up: 0KB/s
|
||||
Status: prebuf | Peers: 4 | Down: 540KB/s | Up: 0KB/s
|
||||
Status: dl | Peers: 4 | Down: 887KB/s | Up: 0KB/s
|
||||
Status: dl | Peers: 4 | Down: 957KB/s | Up: 0KB/s
|
||||
Status: dl | Peers: 4 | Down: 887KB/s | Up: 0KB/s
|
||||
Status: dl | Peers: 4 | Down: 870KB/s | Up: 1KB/s
|
||||
Status: dl | Peers: 4 | Down: 828KB/s | Up: 2KB/s
|
||||
INFO 2019-05-10 19:31:23,590 playstream3.py 57 Client exit.
|
||||
```
|
||||
|
||||
Send <kbd>Ctrl + C</kbd> to exit.
|
||||
|
||||
## Reference
|
||||
- [Ace Stream Wiki (English)](http://wiki.acestream.org/wiki/index.php/%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0/en).
|
||||
- Binary downloads: http://wiki.acestream.org/wiki/index.php/Download.
|
||||
- Ubuntu install notes: http://wiki.acestream.org/wiki/index.php/Install_Ubuntu.
|
||||
- HTTP API usage: http://wiki.acestream.org/wiki/index.php/Engine_HTTP_API.
|
||||
- `playstream.py` routines inspired by: https://github.com/magnetikonline/docker-acestream-server.
|
||||
9
build.sh
Executable file
9
build.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DIRNAME=$(dirname "$0")
|
||||
DOCKER_IMAGE_NAME="jackwzh/acestream-server"
|
||||
|
||||
docker build \
|
||||
--tag "$DOCKER_IMAGE_NAME" \
|
||||
--force-rm \
|
||||
"$DIRNAME"
|
||||
279
playstream.py
Executable file
279
playstream.py
Executable file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
# File: /playstream3.py
|
||||
# Created Date: Monday April 29th 2019
|
||||
# -----
|
||||
# Last Modified: Sunday May 12th 2019 7:22:55 pm
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, server_host, server_port, multi_players=False):
|
||||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
|
||||
self.engine_version = ""
|
||||
self.engine_version_code = 0
|
||||
|
||||
self.multi_players = multi_players
|
||||
self.poll_time = 2
|
||||
|
||||
self.running = False
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
def stop(sig_num, stack_frame):
|
||||
self.running = False
|
||||
|
||||
self.running = True
|
||||
logging.info("Client starts.")
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type=None, exc_value=None, traceback=None):
|
||||
if any([exc_type, exc_value, traceback]):
|
||||
logging.error(repr(exc_type))
|
||||
logging.error(repr(exc_value))
|
||||
logging.error(repr(traceback))
|
||||
logging.info("Client exit.")
|
||||
return True
|
||||
|
||||
def _api_request(self, url: str) -> Dict:
|
||||
"""Send request to acestream server and return response json dict
|
||||
|
||||
Args:
|
||||
url: api url
|
||||
Returns:
|
||||
dict: response json data
|
||||
"""
|
||||
response = urllib.request.urlopen(url)
|
||||
return json.loads(response.read().decode())
|
||||
|
||||
def _check_server_availability(self) -> bool:
|
||||
"""Check server availability before start streaming
|
||||
|
||||
Returns:
|
||||
bool: wether server is avaliable
|
||||
"""
|
||||
url = "http://{}:{}/webui/api/service?method=get_version&format=jsonp&callback=".format(
|
||||
self.server_host, self.server_port)
|
||||
try:
|
||||
response_dic = self._api_request(url)
|
||||
except:
|
||||
logging.exception("Check server availability failed!")
|
||||
return False
|
||||
else:
|
||||
if response_dic.get("error"):
|
||||
return False
|
||||
self.engine_version = response_dic.get("result").get("version")
|
||||
self.engine_version_code = int(
|
||||
response_dic.get("result").get("code"))
|
||||
|
||||
logging.info("acestream engine version: {}".format(
|
||||
self.engine_version))
|
||||
logging.info("acestream engine version code: {}".format(
|
||||
self.engine_version_code))
|
||||
|
||||
return True
|
||||
|
||||
def start_streaming(self, content_id: str) -> Tuple[str, str]:
|
||||
"""Start streaming content ID
|
||||
|
||||
Args:
|
||||
content_id: acestream content ID
|
||||
Returns:
|
||||
playback_url: playback url for media player to stream
|
||||
stat_url: stat url for client to get stat info of acestream engine
|
||||
"""
|
||||
if self.multi_players:
|
||||
# generate a player id to support multi players playing
|
||||
player_id = hashlib.sha1(content_id.encode()).hexdigest()
|
||||
url = 'http://{}:{}/ace/getstream?format=json&id={}&pid={}'.format(
|
||||
self.server_host, self.server_port, content_id, player_id)
|
||||
else:
|
||||
url = 'http://{}:{}/ace/getstream?format=json&id={}'.format(
|
||||
self.server_host, self.server_port, content_id)
|
||||
|
||||
try:
|
||||
response_dic = self._api_request(url)
|
||||
except:
|
||||
logging.exception(
|
||||
"Parsing server http response failed while starting streaming!"
|
||||
)
|
||||
return "", ""
|
||||
else:
|
||||
playback_url = response_dic.get('response').get("playback_url")
|
||||
stat_url = response_dic.get('response').get("stat_url")
|
||||
return playback_url, stat_url
|
||||
|
||||
def _start_media_player(self, media_player: str,
|
||||
playback_url: str) -> bool:
|
||||
"""Start media to get stream from acestream server
|
||||
|
||||
Args:
|
||||
media_player: media player cli program name
|
||||
playback_url: acestream server playback url
|
||||
Return:
|
||||
bool: whether media player stared successfully
|
||||
"""
|
||||
# change this if the predefined command is not suitable for your player
|
||||
cmd = [media_player, playback_url]
|
||||
|
||||
try:
|
||||
process = subprocess.run(cmd)
|
||||
process.check_returncode()
|
||||
except subprocess.CalledProcessError:
|
||||
logging.exception("{} didn't exit normally!".format(media_player))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _monitor_stream_status(self, stat_url: str) -> None:
|
||||
"""Keep monitor stream stat status
|
||||
|
||||
Args:
|
||||
stat_url: acestream server stat url
|
||||
"""
|
||||
|
||||
def stream_stats_message(response: dict) -> str:
|
||||
return 'Status: {} | Peers: {:>3} | Down: {:>4}KB/s | Up: {:>4}KB/s'.format(
|
||||
response.get('response', {
|
||||
'status': 0
|
||||
}).get('status', ""),
|
||||
response.get('response', {
|
||||
'peers': 0
|
||||
}).get('peers', 0),
|
||||
response.get('response', {
|
||||
'speed_down': 0
|
||||
}).get('speed_down', 0),
|
||||
response.get('response', {
|
||||
'speed_up': 0
|
||||
}).get('speed_up', 0))
|
||||
|
||||
while (self.running):
|
||||
print(stream_stats_message(self._api_request(stat_url)))
|
||||
|
||||
time.sleep(self.poll_time)
|
||||
|
||||
def run(self, content_id: str, media_player: str) -> bool:
|
||||
"""A simplified api for running whole process easily
|
||||
|
||||
Args:
|
||||
content_id: acestream content ID
|
||||
media_player: media player to play the stream
|
||||
Returns:
|
||||
bool: whether client run successfully
|
||||
"""
|
||||
if not self._check_server_availability():
|
||||
logging.error(
|
||||
"Server is not available. Please check server status")
|
||||
return False
|
||||
logging.info("Acestream server is available")
|
||||
|
||||
playback_url, stat_url = self.start_streaming(content_id)
|
||||
if not playback_url or not stat_url:
|
||||
return False
|
||||
logging.debug("Server playback url: {}".format(playback_url))
|
||||
logging.debug("Server stat url: {}".format(stat_url))
|
||||
|
||||
if not self._start_media_player(media_player, playback_url):
|
||||
return False
|
||||
|
||||
self._monitor_stream_status(stat_url)
|
||||
|
||||
|
||||
DEFAULT_SERVER_HOSTNAME = '127.0.0.1'
|
||||
DEFAULT_SERVER_PORT = 6878
|
||||
DEFAULT_MEDIA_PLAYER = "iina"
|
||||
SERVER_POLL_TIME = 2
|
||||
SERVER_STATUS_STREAM_ACTIVE = 'dl'
|
||||
FORMAT = '%(levelname)s %(asctime)-15s %(filename)s %(lineno)-8s %(message)s'
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse comand line arguments
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: command line args
|
||||
"""
|
||||
# create parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Instructs server to commence a given content ID. '
|
||||
'Will execute a local media player once playback has started.')
|
||||
|
||||
parser.add_argument(
|
||||
'--content-id',
|
||||
help='content ID to stream',
|
||||
metavar='HASH',
|
||||
required=True,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--player',
|
||||
help='media player to execute once stream active',
|
||||
default=DEFAULT_MEDIA_PLAYER,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--server',
|
||||
default=DEFAULT_SERVER_HOSTNAME,
|
||||
help='server hostname, defaults to %(default)s',
|
||||
metavar='HOSTNAME',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--port',
|
||||
default=DEFAULT_SERVER_PORT,
|
||||
help='server HTTP API port, defaults to %(default)s',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--multi-players',
|
||||
action="store_true",
|
||||
help='play stream in multiple players mode, defaults to %(default)s',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--debug',
|
||||
action="store_true",
|
||||
help='run client in debug mode',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not re.match(r'^[a-f0-9]{40}$', args.content_id):
|
||||
# if content id is not a valid hash, quit program
|
||||
logging.error('Invalid content ID: [{}]'.format(args.content_id))
|
||||
sys.exit(1)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
|
||||
if args.debug:
|
||||
logging.basicConfig(format=FORMAT, level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(format=FORMAT, level=logging.INFO)
|
||||
|
||||
with Client(args.server, args.port) as client:
|
||||
client.run(args.content_id, args.player)
|
||||
13
run.sh
Executable file
13
run.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DOCKER_IMAGE_NAME="jackwzh/acestream-server"
|
||||
SERVER_HTTP_PORT="6878"
|
||||
|
||||
docker run \
|
||||
--name ace-server \
|
||||
--publish "$SERVER_HTTP_PORT:$SERVER_HTTP_PORT" \
|
||||
-p 8621:8621 \
|
||||
--rm \
|
||||
--mount type=tmpfs,target=/dev/disk/by-id,tmpfs-mode=660,tmpfs-size=4k \
|
||||
--mount type=tmpfs,target=/root/.ACEStream,tmpfs-mode=660,tmpfs-size=8192m \
|
||||
"$DOCKER_IMAGE_NAME"
|
||||
Reference in New Issue
Block a user